├── LICENSE ├── Providers └── GrpcServiceProvider.php ├── README.md ├── RoadRunner └── .rr.yaml ├── composer.json ├── config └── grpc.php └── src ├── BaseStubWrapper.php ├── Commands ├── GrpcClientMakeCommand.php └── stubs │ └── grpc-client.stub ├── Contracts ├── Kernel.php ├── ServiceInvoker.php └── ServiceWrapper.php ├── Exceptions └── GrpcServiceException.php ├── GrpcClient.php ├── Kernel.php ├── LaravelGrpcServiceProvider.php ├── LaravelServiceInvoker.php ├── ReflectionServiceWrapper.php └── worker.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vandar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Providers/GrpcServiceProvider.php: -------------------------------------------------------------------------------- 1 | server = $this->app->make(Kernel::class); 29 | //$this->bindGrpc(NotificationRepository::class, NotificationService::class); 30 | } 31 | 32 | /** 33 | * Bootstrap any application services. 34 | * 35 | * @return void 36 | */ 37 | public function boot() 38 | { 39 | } 40 | 41 | /** 42 | * Register a binding with the container. 43 | * 44 | * @param string $abstract 45 | * @param Closure|string|null $concrete 46 | * @param bool $shared 47 | * @return void 48 | * @throws ReflectionException 49 | */ 50 | public function bindGrpc(string $abstract, Closure|string $concrete = null, bool $shared = false) 51 | { 52 | $this->app->bind($abstract, $concrete, $shared); 53 | if (!is_string($concrete) || !class_exists($concrete)) { 54 | return; 55 | } 56 | if ((new $concrete()) instanceof ServiceInterface) { 57 | $this->server->registerService($abstract); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Grpc 2 | 3 | This package is prepared for implementing `GRPC` on the server side and client side in Laravel to implement the 4 | microservice structure. 5 | 6 | Please follow the steps below to set up the desired section, Also, at the end of the page, examples are given that will 7 | give you a better understanding 8 | 9 | ## Installation 10 | 11 | ```bash 12 | composer require vandarpay/laravel-grpc 13 | ``` 14 | 15 | ### Publish Necessary File 16 | 17 | ```bash 18 | php artisan vendor:publish --provider=" vandarpay\LaravelGrpc\LaravelGrpcServiceProvider" 19 | ``` 20 | 21 | ## Requirement 22 | 23 | - PHP 8.1 24 | - [Install](https://github.com/protocolbuffers/protobuf/tree/master/php) `protobuf-ext` . 25 | - Install `grpc-ext` . 26 | 27 | _________________ 28 | 29 | # Server Side 30 | 31 | This package use RoadRunner for manage process and implement grpc server, RoadRunner is a high-performance PHP 32 | application server, load-balancer, and process manager written in Golang. 33 | 34 | ## Installation RoadRunner Binary 35 | 36 | You can also install RoadRunner automatically using command shipped with the composer package, run: 37 | 38 | ```bash 39 | ./vendor/bin/rr get-binary 40 | ``` 41 | 42 | Server binary will be available at the root of your project. 43 | 44 | > PHP's extensions php-curl and php-zip are required to download RoadRunner automatically. PHP's extensions php-sockets need to be installed to run roadrunner. Check with php --modules your installed extensions. 45 | 46 | ## Environments 47 | 48 | Please add this environment variable to `.env` file 49 | 50 | #Available value : panic, error, warn, info, debug. Default: debug 51 | ROAD_RUNNER_LOG_LEVEL=debug 52 | GRPC_XDEBUG=0 # 1,0 53 | GRPC_SERVER=tcp://127.0.0.1:6001 54 | GRPC_WORKER_MAX_JOBS=0 55 | GRPC_WORKER_NUM_WORKERS=2 56 | 57 | - #### GRPC_XDEBUG : 58 | 59 | To activate xDebug make sure to set the `xdebug.mode=debug` in your `php.ini`. 60 | 61 | To enable xDebug in your application make sure to set `ENV` variable `GRPC_XDEBUG` 62 | 63 | - #### GRPC_SERVER : 64 | 65 | Consider the address you want for the GRPC server 66 | 67 | - #### GRPC_WORKER_MAX_JOBS : 68 | 69 | Maximal count of worker executions. Zero (or nothing) means no limit.(Default: 0) 70 | 71 | - #### GRPC_WORKER_NUM_WORKERS : 72 | 73 | How many worker processes will be started. Zero (or nothing) means the number of logical CPUs.(Default : 0) 74 | 75 | ## Make New Service 76 | 77 | ```bash 78 | php artisan make:service {service-name} --grpc --language=fa 79 | ``` 80 | 81 | ## Compile proto files 82 | 83 | To start, create a proto file named `echo.proto` in path `app/Protobuf` and copy the following content into it, if this 84 | folder does not exist, create it 85 | 86 | ``` 87 | syntax = "proto3"; 88 | 89 | package services; 90 | 91 | option php_namespace = "GrpcServices\\Echo\\Messages"; 92 | option php_metadata_namespace = "GrpcServices\\Echo"; 93 | 94 | service Echo { 95 | rpc Ping (PingMessage) returns (PingMessage) { 96 | } 97 | } 98 | 99 | message PingMessage { 100 | string msg = 1; 101 | } 102 | ``` 103 | 104 | Note that the following 3 lines of this file should not be changed 105 | 106 | ``` 107 | package services; 108 | 109 | option php_namespace = "GrpcServices\\Echo\\Messages"; 110 | option php_metadata_namespace = "GrpcServices\\Echo"; 111 | ``` 112 | 113 | ### Install protobuf compiler 114 | ```bash 115 | sudo apt install -y protobuf-compiler 116 | ``` 117 | 118 | Next, after installing `protoc` program, execute the following command to create the required files for connection 119 | 120 | ```bash 121 | protoc --proto_path=app/Protobuf --php_out=./app/Protobuf echo.proto 122 | ``` 123 | 124 | After executing command `protoc`, a folder named `GrpcServices` is created in path `app/Protobuf`, which contains 125 | messages and the main service class 126 | 127 | ## Create First Method 128 | 129 | Add this line to created repository 130 | 131 | ```php 132 | use Spiral\RoadRunner\GRPC\ContextInterface; 133 | use GrpcServices\Echo\Messages\PingMessage; 134 | 135 | public function Ping(ContextInterface $ctx, PingMessage $in): PingMessage; 136 | ``` 137 | 138 | And then in the related service, add the additional method in the repository and put its output according to the 139 | definitions of the `proto` file. 140 | 141 | ## Register Proto File In RoadRunner 142 | 143 | The path of file proto created in the previous step should be added in file `.rr.yaml` in the following way 144 | 145 | ``` 146 | grpc: 147 | listen: ${GRPC_SERVER} 148 | proto: 149 | - "app/Protobuf/notification.proto" 150 | - "proto file path" 151 | ``` 152 | 153 | ## Register Service To GRPC Server 154 | 155 | To register the service created in method `register`, class `App\Providers\GrpcServiceProvider.php`, register your 156 | service as below 157 | 158 | ```php 159 | $this->bindGrpc(PingRepository::class, PingService::class); 160 | ``` 161 | 162 | ## Run RoadRunner Sever 163 | 164 | ```bash 165 | ./rr serve --dotenv .env 166 | ``` 167 | 168 | # Client Side 169 | 170 | To build the communication class on the client side, first, the proto file must be compiled like the server routine, and 171 | the communication class must be created by the following command. 172 | 173 | ```bash 174 | composer require vandarpay/laravel-grpc 175 | php artisan vendor:publish --tag=grpc-config 176 | ``` 177 | 178 | ```bash 179 | php artisan make:grpc-client {service name} 180 | ``` 181 | 182 | The proto file should be in `app/Protobuf` path. And after executing the above command, folder `Clients` will be created 183 | in path `app/Protobuf`. 184 | 185 | After this, the communication settings of the defined service should be placed in the config file `grpc.php` 186 | 187 | ``` 188 | 'notification' => [ 189 | 'host' => env('NOTIFICATION_SERVER_HOST'), 190 | 'authentication' => env('NOTIFICATION_SERVER_AUTHENTICATION','insecure'), // insecure, tls 191 | 'cert' => env('NOTIFICATION_SERVER_CERT','') 192 | ], 193 | ``` 194 | 195 | **Note**: The index of this setting must be equal to the service name 196 | 197 | In this step, transfer the proto file from the server side to the client and create the required files with the proto compiler. 198 | 199 | ```bash 200 | protoc --proto_path=app/Protobuf --php_out=./app/Protobuf notification.proto 201 | ``` 202 | 203 | Below is an example to better understand the GRPC client class 204 | 205 | ```php 206 | setMsg($message . ' In Client ' . rand(0, 9999)); 222 | 223 | return $this->client->simpleRequest('send', $request); 224 | } 225 | } 226 | ``` 227 | 228 | ## Versioning On Service 229 | 230 | Of course, in the development of a service, there are times when there is a need to upgrade the old version, and this 231 | part is fully supported in this package. For this purpose, after considering the necessary folders for the service. 232 | 233 | In this case, the folder structure changes as follows 234 | 235 | ``` 236 | ├── Services 237 | ├── Test 238 | | ├── v1 239 | | | ├── TestException.php 240 | | | ├── TestRepository.php 241 | | | ├── TestTransformer.php 242 | | | └── TestService.php 243 | | └── v2 244 | | ├── TestException.php 245 | | ├── TestRepository.php 246 | | ├── TestTransformer.php 247 | | └── TestService.php 248 | └── AlphaService 249 | ``` 250 | -------------------------------------------------------------------------------- /RoadRunner/.rr.yaml: -------------------------------------------------------------------------------- 1 | # configuration version: https://roadrunner.dev/docs/beep-beep-config/2.x/en 2 | version: '2.7' 3 | 4 | server: 5 | command: "php worker.php" 6 | relay: "pipes" 7 | relay_timeout: "20s" 8 | env: 9 | - XDEBUG_SESSION: ${GRPC_XDEBUG} 10 | 11 | grpc: 12 | listen: ${GRPC_SERVER} 13 | proto: 14 | - "app/Protobuf/notification.proto" 15 | # Maximum send message size 16 | # 17 | # This option is optional. Default value: 50 (MB) 18 | max_send_msg_size: 50 19 | 20 | # Maximum receive message size 21 | # 22 | # This option is optional. Default value: 50 (MB) 23 | max_recv_msg_size: 50 24 | # MaxConnectionIdle is a duration for the amount of time after which an 25 | # idle connection would be closed by sending a GoAway. Idleness duration is 26 | # defined since the most recent time the number of outstanding RPCs became 27 | # zero or the connection establishment. 28 | # 29 | # This option is optional. Default value: infinity. 30 | max_connection_idle: 0s 31 | 32 | # MaxConnectionAge is a duration for the maximum amount of time a 33 | # connection may exist before it will be closed by sending a GoAway. A 34 | # random jitter of +/-10% will be added to MaxConnectionAge to spread out 35 | # connection storms. 36 | # 37 | # This option is optional. Default value: infinity. 38 | max_connection_age: 0s 39 | 40 | # MaxConnectionAgeGrace is an additive period after MaxConnectionAge after 41 | # which the connection will be forcibly closed. 42 | max_connection_age_grace: 0s 43 | 44 | # MaxConnectionAgeGrace is an additive period after MaxConnectionAge after 45 | # which the connection will be forcibly closed. 46 | # 47 | # This option is optional: Default value: 10 48 | max_concurrent_streams: 10 49 | 50 | # After a duration of this time if the server doesn't see any activity it 51 | # pings the client to see if the transport is still alive. 52 | # If set below 1s, a minimum value of 1s will be used instead. 53 | # 54 | # This option is optional. Default value: 2h 55 | ping_time: 1s 56 | 57 | # After having pinged for keepalive check, the server waits for a duration 58 | # of Timeout and if no activity is seen even after that the connection is 59 | # closed. 60 | # 61 | # This option is optional. Default value: 20s 62 | timeout: 200s 63 | pool: 64 | # How many worker processes will be started. Zero (or nothing) means the number of logical CPUs. 65 | # 66 | # Default: 0 67 | num_workers: ${GRPC_WORKER_NUM_WORKERS} 68 | # Maximal count of worker executions. Zero (or nothing) means no limit. 69 | # 70 | # Default: 0 71 | max_jobs: ${GRPC_WORKER_MAX_JOBS} 72 | # Timeout for worker allocation. Zero means no limit. 73 | # 74 | # Default: 60s 75 | allocate_timeout: 60s 76 | # Timeout for worker destroying before process killing. Zero means no limit. 77 | # 78 | # Default: 60s 79 | destroy_timeout: 60 80 | logs: 81 | #Available value : panic, error, warn, info, debug. Default: debug 82 | level: ${ROAD_RUNNER_LOG_LEVEL} 83 | 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vandarpay/laravel-grpc", 3 | "description": "GRPC Laravel Package", 4 | "type": "library", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^8.1", 8 | "vandarpay/service-repository": "^0.1", 9 | "ext-protobuf": "*", 10 | "ext-grpc": "*", 11 | "spiral/roadrunner": "^2.11", 12 | "spiral/roadrunner-laravel": "^5.9", 13 | "spiral/roadrunner-grpc": "^2.0", 14 | "nyholm/psr7": "^1.5", 15 | "google/common-protos": "^3.0", 16 | "grpc/grpc": "^1.36", 17 | "google/protobuf": "^3.11" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Hamid Mahmoudpour", 22 | "email": "mp.hamid@yahoo.com" 23 | } 24 | ], 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "vandarpay\\LaravelGrpc\\LaravelGrpcServiceProvider" 29 | ] 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "GrpcServices\\": "../../../app/Protobuf/GrpcServices", 35 | "vandarpay\\LaravelGrpc\\": "src/" 36 | } 37 | }, 38 | "minimum-stability": "dev" 39 | } 40 | -------------------------------------------------------------------------------- /config/grpc.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'Notification' => [ 5 | 'host' => '127.0.0.1:9009', 6 | 'authentication' => 'insecure', // insecure, tls 7 | 'cert' => '' 8 | ] 9 | ], 10 | ]; 11 | 12 | -------------------------------------------------------------------------------- /src/BaseStubWrapper.php: -------------------------------------------------------------------------------- 1 | serviceName; 21 | } 22 | 23 | /** 24 | * @param string $serviceName 25 | * @return $this 26 | */ 27 | public function setServiceName(string $serviceName): static 28 | { 29 | $this->serviceName = $serviceName; 30 | return $this; 31 | } 32 | 33 | /** 34 | * @param string $methodName 35 | * @param $request 36 | * @return mixed 37 | * @throws GrpcServiceException 38 | * @throws Exception 39 | */ 40 | public function simpleRequest(string $methodName, $request) 41 | { 42 | $grpcRequest = $this->_simpleRequest( 43 | '/' . $this->getServiceName() . '/' . $methodName, 44 | $request, 45 | [$request::class, 'decode'], 46 | [], 47 | [] 48 | ); 49 | [$response, $status] = $grpcRequest->wait(); 50 | if ($status->code == 99) { 51 | throw new GrpcServiceException(json_decode($status->details, true)); 52 | } 53 | if ($status->code != 0) { 54 | throw new Exception($status->details); 55 | } 56 | return $response; 57 | } 58 | 59 | /** 60 | * @param string $methodName 61 | * @param $request 62 | * @return Generator|mixed 63 | */ 64 | public function streamRequest(string $methodName, $request): mixed 65 | { 66 | $grpcRequest = $this->_serverStreamRequest( 67 | '/' . $this->getServiceName() . '/' . $methodName, 68 | $request, 69 | [$request::class, 'decode'], 70 | [], 71 | [] 72 | ); 73 | return $grpcRequest->responses(); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/Commands/GrpcClientMakeCommand.php: -------------------------------------------------------------------------------- 1 | rootNamespace(), '', $name); 22 | return $this->laravel['path'] . '/' . str_replace('\\', '/', $name). 'Client.php'; 23 | } 24 | 25 | protected function getStub(): string 26 | { 27 | return __DIR__ . '/stubs/grpc-client.stub'; 28 | } 29 | 30 | 31 | protected function getDefaultNamespace($rootNamespace) 32 | { 33 | return "{$rootNamespace}\\Protobuf\Clients" ; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/stubs/grpc-client.stub: -------------------------------------------------------------------------------- 1 | message = $exceptionArray['message']; 15 | $this->code = $exceptionArray['code']; 16 | $this->exception_code = $exceptionArray['app_code']; 17 | } 18 | /** 19 | * @return string 20 | */ 21 | public function getAppCode() : string 22 | { 23 | return $this->exception_code; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/GrpcClient.php: -------------------------------------------------------------------------------- 1 | service)); 20 | if(is_null($config)){ 21 | throw new Exception('Configuration Failed ('.$this->service.')'); 22 | } 23 | $authenticationMethod = 'create'.ucfirst($config['authentication']).'Credentials'; 24 | $this->client = new BaseStubWrapper($config['host'], [ 25 | 'credentials' => $this->{$authenticationMethod}($config['cert']??''), 26 | ]); 27 | $this->client->setServiceName($this->service); 28 | } 29 | 30 | /** 31 | * Create tls credential 32 | * @param string $certPath 33 | * @return ChannelCredentials 34 | */ 35 | private function createTlsCredentials(string $certPath): ChannelCredentials 36 | { 37 | return ChannelCredentials::createSsl(file_get_contents(base_path($certPath))); 38 | } 39 | 40 | /** 41 | * Create insecure credential 42 | * @param string $certPath 43 | * @return null 44 | */ 45 | private function createInsecureCredentials(string $certPath) 46 | { 47 | return ChannelCredentials::createInsecure(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | > 45 | * } 46 | */ 47 | class Kernel implements KernelContract 48 | { 49 | /** 50 | * The application implementation. 51 | * 52 | * @var Application 53 | */ 54 | protected Application $app; 55 | 56 | /** 57 | * Service invoker. 58 | * 59 | * @var LaravelServiceInvoker 60 | */ 61 | protected LaravelServiceInvoker $invoker; 62 | 63 | /** 64 | * Services definition. 65 | * 66 | * @var array 67 | */ 68 | protected array $services = []; 69 | 70 | /** 71 | * The bootstrap classes for the application. 72 | * 73 | * @var array 74 | */ 75 | protected array $bootstrappers = [ 76 | LoadEnvironmentVariables::class, 77 | LoadConfiguration::class, 78 | HandleExceptions::class, 79 | RegisterFacades::class, 80 | SetRequestForConsole::class, 81 | RegisterProviders::class, 82 | BootProviders::class, 83 | ]; 84 | /** 85 | * @var ServerOptions 86 | */ 87 | private array $options; 88 | 89 | /**. 90 | * Create a new GRPC kernel instance. 91 | * 92 | * @param Application $app 93 | * @param ServiceInvoker $invoker 94 | * @param array $options 95 | */ 96 | public function __construct(Application $app, ServiceInvoker $invoker, array $options = []) 97 | { 98 | $this->app = $app; 99 | $this->invoker = $invoker; 100 | $this->options = $options; 101 | } 102 | 103 | /** 104 | * Register available services. 105 | * @param string $interface 106 | * @return KernelContract 107 | * @throws ReflectionException|BindingResolutionException 108 | */ 109 | public function registerService(string $interface): KernelContract 110 | { 111 | $service = new ReflectionServiceWrapper($this->invoker, $this->app->make($interface)); 112 | $this->services[$service->getName()] = $service; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * @param Worker $worker 119 | * @param string $body 120 | * @param string $headers 121 | * @psalm-suppress InaccessibleMethod 122 | */ 123 | private function workerSend(Worker $worker, string $body, string $headers): void 124 | { 125 | $worker->respond(new Payload($body, $headers)); 126 | } 127 | 128 | /** 129 | * @param Worker $worker 130 | * @param string $message 131 | */ 132 | private function workerError(Worker $worker, string $message): void 133 | { 134 | $worker->error($message); 135 | } 136 | 137 | /** 138 | * If server runs in debug mode 139 | * 140 | * @return bool 141 | */ 142 | private function isDebugMode(): bool 143 | { 144 | $debug = false; 145 | 146 | if (isset($this->options['debug'])) { 147 | $debug = filter_var($this->options['debug'], FILTER_VALIDATE_BOOLEAN); 148 | } 149 | 150 | return $debug; 151 | } 152 | 153 | /** 154 | * Serve GRPC over given RoadRunner worker. 155 | * 156 | * @param Worker|null $worker 157 | * @param callable|null $finalize 158 | */ 159 | public function serve(Worker $worker = null, callable $finalize = null): void 160 | { 161 | $this->bootstrap(); 162 | $worker ??= Worker::create(); 163 | while (true) { 164 | $request = $worker->waitPayload(); 165 | if ($request === null) { 166 | return; 167 | } 168 | 169 | try { 170 | /** @var ContextResponse $context */ 171 | $context = Json::decode($request->header); 172 | 173 | [$answerBody, $answerHeaders] = $this->tick($request->body, $context); 174 | $this->workerSend($worker, $answerBody, $answerHeaders); 175 | } catch (GRPCExceptionInterface $e) { 176 | $this->workerError($worker, $this->packError($e)); 177 | } catch (Throwable $e) { 178 | $this->workerError($worker, $this->isDebugMode() ? (string)$e : $e->getMessage()); 179 | } finally { 180 | if ($finalize !== null) { 181 | isset($e) ? $finalize($e) : $finalize(); 182 | } 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * @param string $body 189 | * @param ContextResponse $data 190 | * @return array{ 0: string, 1: string } 191 | * @throws JsonException 192 | * @throws Throwable 193 | */ 194 | private function tick(string $body, array $data): array 195 | { 196 | $context = (new Context($data['context'])) 197 | ->withValue(ResponseHeaders::class, new ResponseHeaders()); 198 | 199 | $response = $this->invoke($data['service'], $data['method'], $context, $body); 200 | 201 | /** @var ResponseHeaders|null $responseHeaders */ 202 | $responseHeaders = $context->getValue(ResponseHeaders::class); 203 | $responseHeadersString = $responseHeaders ? $responseHeaders->packHeaders() : '{}'; 204 | 205 | return [$response, $responseHeadersString]; 206 | } 207 | 208 | /** 209 | * Bootstrap the application for HTTP requests. 210 | * 211 | * @return void 212 | */ 213 | public function bootstrap(): void 214 | { 215 | if (!$this->app->hasBeenBootstrapped()) { 216 | $this->app->bootstrapWith($this->bootstrappers()); 217 | } 218 | } 219 | 220 | /** 221 | * Get the Laravel application instance. 222 | * 223 | * @return Application 224 | */ 225 | public function getApplication(): Application 226 | { 227 | return $this->app; 228 | } 229 | 230 | /** 231 | * Invoke service method with binary payload and return the response. 232 | * 233 | * @param string $service 234 | * @param string $method 235 | * @param ContextInterface $context 236 | * @param string $body 237 | * @return string 238 | * @throws GRPCException 239 | */ 240 | protected function invoke(string $service, string $method, ContextInterface $context, string $body): string 241 | { 242 | if (!isset($this->services[$service])) { 243 | throw NotFoundException::create("Service `{$service}` not found.", StatusCode::NOT_FOUND); 244 | } 245 | 246 | return $this->services[$service]->invoke($method, $context, $body); 247 | } 248 | 249 | /** 250 | * Get the bootstrap classes for the application. 251 | * 252 | * @return array 253 | */ 254 | protected function bootstrappers() 255 | { 256 | return $this->bootstrappers; 257 | } 258 | 259 | /** 260 | * Packs exception message and code into one string. 261 | * 262 | * Internal agreement: 263 | * 264 | * Details will be sent as serialized google.protobuf.Any messages after code and exception message separated with |:| delimeter. 265 | * 266 | * @param GRPCException $e 267 | * @return string 268 | */ 269 | protected function packError(GRPCException $e): string 270 | { 271 | $data = [$e->getCode(), $e->getMessage()]; 272 | 273 | foreach ($e->getDetails() as $detail) { 274 | /** 275 | * @var Message $detail 276 | */ 277 | 278 | $anyMessage = new Any(); 279 | 280 | $anyMessage->pack($detail); 281 | 282 | $data[] = $anyMessage->serializeToString(); 283 | } 284 | 285 | return implode("|:|", $data); 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/LaravelGrpcServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerConfig(); 13 | } 14 | 15 | public function boot() 16 | { 17 | if ($this->app->runningInConsole()) { 18 | $this->registerCommands(); 19 | $this->publishConfigs(); 20 | } 21 | } 22 | 23 | protected function registerConfig(): void 24 | { 25 | $this->mergeConfigFrom(__DIR__ . '/../config/grpc.php', 'grpc'); 26 | } 27 | 28 | protected function registerCommands(): void 29 | { 30 | $this->commands([ 31 | GrpcClientMakeCommand::class, 32 | ]); 33 | } 34 | 35 | protected function publishConfigs(): void 36 | { 37 | $this->publishes([ 38 | __DIR__ . '/../config/grpc.php' => config_path('grpc.php'), 39 | ], 'grpc-config'); 40 | $this->publishes([ 41 | __DIR__ . '/worker.php' => base_path('worker.php'), 42 | ], 'grpc-worker'); 43 | $this->publishes([ 44 | __DIR__ . '/../Providers/GrpcServiceProvider.php' => app_path('Providers/GrpcServiceProvider.php'), 45 | ], 'grpc-provider'); 46 | $this->publishes([ 47 | __DIR__ . '/../RoadRunner/.rr.yaml' => base_path('.rr.yaml'), 48 | ], 'roadrunner-config'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/LaravelServiceInvoker.php: -------------------------------------------------------------------------------- 1 | app = $app; 53 | } 54 | 55 | /** 56 | * Get the Laravel application instance. 57 | * 58 | * @return Application 59 | */ 60 | public function getApplication(): Application 61 | { 62 | return $this->app; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | public function invoke(ServiceInterface $service, Method $method, ContextInterface $ctx, ?string $input): string 69 | { 70 | try { 71 | /** @var callable $callable */ 72 | $callable = [$service, $method->getName()]; 73 | 74 | /** @var Message $message */ 75 | $message = $callable($ctx, $this->makeInput($method, $input)); 76 | } catch (ServiceException $e) { 77 | $exceptionArray = [ 78 | 'code' => $e->getCode(), 79 | 'message' => $e->getMessage(), 80 | 'app_code' => $e->getAppCode() 81 | ]; 82 | throw InvokeException::create(json_encode($exceptionArray), 99, $e); 83 | } 84 | // Note: This validation will only work if the 85 | // assertions option ("zend.assertions") is enabled. 86 | assert($this->assertResultType($method, $message)); 87 | 88 | try { 89 | return $message->serializeToString(); 90 | } catch (Throwable $e) { 91 | throw InvokeException::create($e->getMessage(), StatusCode::INTERNAL, $e); 92 | } 93 | } 94 | 95 | /** 96 | * Checks that the result from the GRPC service method returns the 97 | * Message object. 98 | * 99 | * @param Method $method 100 | * @param mixed $result 101 | * @return bool 102 | * @throws BadFunctionCallException 103 | */ 104 | private function assertResultType(Method $method, $result): bool 105 | { 106 | if (!$result instanceof Message) { 107 | $type = is_object($result) ? get_class($result) : get_debug_type($result); 108 | 109 | throw new BadFunctionCallException( 110 | sprintf(self::ERROR_METHOD_RETURN, $method->getName(), Message::class, $type) 111 | ); 112 | } 113 | 114 | return true; 115 | } 116 | 117 | /** 118 | * @param Method $method 119 | * @param string|null $body 120 | * @return Message 121 | * @throws InvokeException 122 | */ 123 | private function makeInput(Method $method, ?string $body): Message 124 | { 125 | try { 126 | $class = $method->getInputType(); 127 | 128 | // Note: This validation will only work if the 129 | // assertions option ("zend.assertions") is enabled. 130 | assert($this->assertInputType($method, $class)); 131 | 132 | /** @psalm-suppress UnsafeInstantiation */ 133 | $in = new $class(); 134 | 135 | if ($body !== null) { 136 | $in->mergeFromString($body); 137 | } 138 | 139 | return $in; 140 | } catch (Throwable $e) { 141 | throw InvokeException::create($e->getMessage(), StatusCode::INTERNAL, $e); 142 | } 143 | } 144 | 145 | /** 146 | * Checks that the input of the GRPC service method contains the 147 | * Message object. 148 | * 149 | * @param Method $method 150 | * @param string $class 151 | * @return bool 152 | * @throws InvalidArgumentException 153 | */ 154 | private function assertInputType(Method $method, string $class): bool 155 | { 156 | if (!is_subclass_of($class, Message::class)) { 157 | throw new InvalidArgumentException( 158 | sprintf(self::ERROR_METHOD_IN_TYPE, $method->getName(), Message::class, $class) 159 | ); 160 | } 161 | 162 | return true; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ReflectionServiceWrapper.php: -------------------------------------------------------------------------------- 1 | invoker = $invoker; 59 | $this->interface = $interface; 60 | 61 | $this->configure($interface::class); 62 | } 63 | 64 | /** 65 | * Retrive service name. 66 | * 67 | * @return string 68 | */ 69 | public function getName(): string 70 | { 71 | return $this->name; 72 | } 73 | 74 | /** 75 | * Retrieve public methods. 76 | * 77 | * @return array 78 | */ 79 | public function getMethods(): array 80 | { 81 | return $this->methods; 82 | } 83 | 84 | /** 85 | * @inheritdoc 86 | */ 87 | public function invoke(string $method, ContextInterface $ctx, ?string $input): string 88 | { 89 | if (!isset($this->methods[$method])) { 90 | throw new NotFoundException("Method `{$method}` not found in service `{$this->name}`."); 91 | } 92 | return $this->invoker->invoke($this->interface, $this->methods[$method], $ctx, $input); 93 | } 94 | 95 | /** 96 | * Configure service name and methods. 97 | * 98 | * @param string $interface 99 | * 100 | * @throws ServiceException|ReflectionException 101 | */ 102 | protected function configure(string $interface) 103 | { 104 | try { 105 | $r = new ReflectionClass($interface); 106 | if (!$r->hasConstant('NAME')) { 107 | throw new ServiceException( 108 | "Invalid service interface `{$interface}`, constant `NAME` not found." 109 | ); 110 | } 111 | $this->name = $r->getConstant('NAME'); 112 | } catch (ReflectionException $e) { 113 | throw new ServiceException( 114 | "Invalid service interface `{$interface}`.", 115 | StatusCode::INTERNAL, 116 | $e 117 | ); 118 | } 119 | 120 | // list of all available methods and their object types 121 | $this->methods = $this->fetchMethods($interface); 122 | } 123 | 124 | /** 125 | * @param string $interface 126 | * @return array 127 | * @throws ReflectionException 128 | */ 129 | protected function fetchMethods(string $interface): array 130 | { 131 | $reflection = new ReflectionClass($interface); 132 | 133 | $methods = []; 134 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 135 | if (Method::match($method)) { 136 | $methods[$method->getName()] = Method::parse($method); 137 | } 138 | } 139 | 140 | return $methods; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/worker.php: -------------------------------------------------------------------------------- 1 | singleton(KernelContract::class, Kernel::class); 19 | $app->singleton(ServiceInvoker::class, LaravelServiceInvoker::class); 20 | 21 | $kernel = $app->make(KernelContract::class); 22 | 23 | $app->register(GrpcServiceProvider::class); 24 | 25 | $w = new Worker(new StreamRelay(STDIN, STDOUT)); 26 | $kernel->serve($w); 27 | --------------------------------------------------------------------------------