# ProtoBuf on Laravel 實作 proto 建置, gRPC Server 及 gRPC Client。 以下內容是假定在 Laravel 已運行良好情況下開始設置。 實作環境 - OS: Ubunto 20.04 - PHP: 8.04 - Laravel: 8 ### 運行環境需要的指令及延伸套件 - Install PHP extension for protobuf ```shell $ sudo pecl install protobuf ``` - Install protoc command ```shell $ sudo apt install protobuf-compiler ``` - Instal protobuf complier grpc ```shell $ sudo apt install protobuf-compiler-grpc ``` ### Laravel 可以新建一個 Laravel 專案來測試,或是在原本已有的 Larvel 專案。 - Install google/protobuf to Laravel ```shell= $ cd {laravel-project-folder} $ composer require google/protobuf ``` - create protobuf folder structure to base path ```shell= $ cd {laravel-project} $ mkdir protobuf $ mkdir protobuf/build $ mkdir protobuf/src ``` - make *.proto file to protobuf/src - read docs https://developers.google.com/protocol-buffers/docs/proto3 example: user.proto ```proto syntax = "proto3"; package mypackage; message UserRequest { uint32 id=1; } message User { uint32 id = 1; string name = 2; string email = 3; string created_at = 4; string updated_at = 5; } ``` - generate protobuf program for php ```shell= $ protoc --proto_path=protobuf \ $ --php_out=protobuf/build protobuf/src/user.proto ``` 這個指令會自動幫你生成 protobuf 需要的 php 檔在protobuf/build 裡,這些檔,我們都不用去動它。 - set psr-4 autoload for php in composer.json ```jsonld= "autoload": { "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/", "Mypackage\\": "protobuf/build/Mypackage/", "": "protobuf/build/" } }, ``` 到這裡,你可以試著用 protoc 產生的物件來試著轉換自己的 Model 資料。 ### gRPC on Laravel #### Install grpc related packages ```shell= $ composer require grpc/grpc $ composer require spiral/roadrunner-grpc ``` #### Download rr-grpc and protoc-gen-php-grpc command Go to https://github.com/spiral/php-grpc/releases to downd latest version 1. save rr-grpc to laravel base folder ```shell= $ cd {laravel-project-folder} $ cp {download-folder}/{extrac-folder}/rr-grpc ./ ``` 2. save protoc-gen-php-grpc to /usr/bin ```shell= $ sudo cp {download-folder}/{extrac-folder}/protoc-gen-php-grpc /usr/bin ``` #### Generator protobuf files 參考上面Laravel的步驟,但這裡要加入 Server 的設定 Example user.proto ```proto syntax = "proto3"; package mypackage; service UserService { rpc getUser(UserRequest) returns(User) {} } message UserRequest { uint32 id=1; } enum Gender { male = 0; female = 1; other = 2; } message User { uint32 id = 1; string name = 2; string email = 3; string created_at = 4; string updated_at = 5; message UserProfile { string nickname = 1; string intro = 3; Gender gender = 4; string birthday = 5; } } ``` 這個範例中加入了 rpc server 相關的設置 ```proto service UserService { rpc getUser(UserRequest) returns(User) {} } ``` 這個在我們產生相關檔案時,會自動幫我們建立 UserService 的 Interfeace, 裡面則有一個 getUser 的 Method 會傳回 User 這個 message 物件。 #### Generate protoBuf php files ```shell= protoc --proto_path=protobuf \ --php_out=protobuf/build \ --grpc_out=protobuf/build \ --plugin=protoc-gen-grpc=/usr/bin/protoc-gen-php-grpc \ protobuf/src/user.proto ``` ``` Usage: protoc [OPTION] PROTO_FILES OPTIONS --proto_path: proto 檔案的所在資料夾 --php_out: php 檔產生處 --grpc_out: 用 protoc-gen-php-grpc 產生的檔案存放位置 --plugin: 使用 plugin, 這裡指定用 protoc-gen-grpc 這個 plugin, 後面再指定指令所在位置 ``` #### 實作 UserService 這裡就依自己在 Laravel 中習慣去建立你自己的物作。 建立 App/Serivces/UserService (app/Services/UserService.php) ```php= <?php namespace App\Services; use App\Models\User as DBUser; use Mypackage\User; use Mypackage\UserRequest; use Mypackage\UserServiceInterface; use Spiral\GRPC; class UserService implements UserServiceInterface { public function getUser(GRPC\ContextInterface $ctx, UserRequest $in): User { $userId = $in->getId(); $user = DBUser::find($userId); $userProto = new User(); $userProto->setId($user->id); $userProto->setName($user->name); $userProto->setEmail($user->email); $userProto->setCreatedAt((string) $user->created_at); $userProto->setUpdatedAt((string) $user->updated_at); return $userProto; } } ``` #### 建立 gRPC 執行的 worker Create worker.php in base path on laravel ```php= <?php use Illuminate\Contracts\Console\Kernel; require_once __DIR__ . '/vendor/autoload.php'; /** 載入 Laravel 核心 */ $app = require_once __DIR__.'/bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); /** 加入 gRPC Server 物件 */ $server = $app->make(\Spiral\GRPC\Server::class); /** 註冊想要的服務 */ $server->registerService(\Mypackage\UserServiceInterface::class, new \App\Services\UserService()); /** 啟始 worker */ $worker = new Spiral\RoadRunner\Worker(new Spiral\Goridge\StreamRelay(STDIN, STDOUT)); $server->serve($worker); ``` #### 設定 gRPC gRPC 是由 rr-grpc 來啟動,它是由 rr 來改寫的,所以一樣吃 .rr.ymal 的設定檔 rr 是 [roadrunner](https://roadrunner.dev/ "roadrunner") 的縮寫,指令即其縮寫。 .rr.yaml ```yaml= grpc: listen: "tcp://0.0.0.0:50051" proto: "protobuf/src/user.proto" workers: command: "php worker.php" pool: numWorkers: 4 ``` #### 啟動 gRPC Server ```shell= $ ./rr-grpc serve -v -d ``` gRPC Server 就起來了,接下來設置 gRPC Client 來測試連線。 #### 建立 gRPC Client for PHP 一樣由 protoc 來自動產生 php code ```shell= protoc --php_out=protobuf/build \ --grpc_out=protobuf/build \ --plugin=protoc-gen-grpc=/usr/bin/grpc_php_plugin \ protobuf/src/user.proto ``` ``` --plugin: 使用 plugin, 這裡指定用 protoc-gen-grpc 這個 plugin, 改用 grpc_php_plugin 指令。 ``` 跟前面產生 UserServiceInterface.php 類似,但這裡產生 UserServiceClient.php #### 開始取得資料 建以一個 user.php 的指令 ```shell= $ cd {laravel-project-folder} $ touch user.php $ chmod 755 user.php ``` user.php 內容 ```php= #!/usr/bin/php <?php require_once __DIR__ . '/vendor/autoload.php'; use Mypackage\UserServiceClient; use Mypackage\UserRequest; use Grpc\ChannelCredentials; $client = new UserServiceClient('0.0.0.0:50051', [ 'credentials' => ChannelCredentials::createInsecure(), ]); if (!isset($argv[1])) { die("Please specify the user id\n"); } $userId = $argv[1]; $request = new UserRequest(); $request->setId($userId); /** @var User $response */ list($response, $status) = $client->getUser($request)->wait(); if ($status->code !== Grpc\STATUS_OK) { echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL; exit(1); } print $response->serializeToJsonString()."\n"; ``` 用指令去取得資料 ```shell= $ ./user.php {userId} ``` exampe: command ```shell= $ ./user.php 22 ``` result ```json= {"id":22,"name":"酈樺美","email":"dexter87@example.org","createdAt":"2021-10-26 15:03:51","updatedAt":"2021-10-26 15:03:51"} ``` ### Error Handling 在建立 proto model 時,加入錯誤輸出的 model,及新的 Response ```proto message Error { uint32 code = 1; string message = 2; message ErrorMeta { string attribute=1; string reason=2; } } message UserResponse { oneof response { User user = 1; Error error = 2; } } ``` 在原本 rpc 的 returns 改成 UserResponse ```proto service UserService { rpc getUser(UserRequest) returns(UserResponse) {} } ``` 整個新的 user.proto 變成 ```proto syntax = "proto3"; package mypackage; service UserService { rpc getUser(UserRequest) returns(UserResponse) {} } message UserRequest { uint32 id=1; } enum Gender { male = 0; female = 1; other = 2; } message User { uint32 id = 1; string name = 2; string email = 3; string contact_email = 4; float percentage = 5; string created_at = 6; string updated_at = 7; message UserProfile { string nickname = 1; string intro = 3; Gender gender = 4; string birthday = 5; } } message UserResponse { oneof response { User user = 1; Error error = 2; } } message Error { uint32 code = 1; string message = 2; message ErrorMeta { string attribute=1; string reason=2; } } ``` 重跑 protoc 指令,產生新的 code ##### UserService 改寫 ```php= <?php namespace App\Services; use App\Models\User as DBUser; use Illuminate\Database\Eloquent\ModelNotFoundException; use Mirrorfiction\Error; use Mirrorfiction\User; use Mirrorfiction\UserRequest; use Mirrorfiction\UserResponse; use Mirrorfiction\UserServiceInterface; use Spiral\GRPC; class UserService implements UserServiceInterface { public function getUser(GRPC\ContextInterface $ctx, UserRequest $in): User { $userId = $in->getId(); try { $user = DBUser::findOrFail($userId); $userProto = new User(); $userProto->setId($user->id); $userProto->setName($user->name); $userProto->setEmail($user->email); $userProto->setCreatedAt((string) $user->created_at); $userProto->setUpdatedAt((string) $user->updated_at); $response->setUser($userProto); } catch(ModelNotFoundException $e) { $error = new Error(); $error->setCode(1)->setMessage($e->getMessage()); $response->setError($error); } return $response; } } ``` ##### Success Result ```json= { "user": { "id": 2, "name": "池家", "email": "rowan.auer@example.net", "created_at": "2021-10-26 15:03:51", "updated_at": "2021-10-26 15:13:38" }, "response": "user" } ``` #### Error Result ```json= { "error": { "code": 1, "message": "No query results for model [App\\Models\\User] 1000" }, "response": "error" } ``` ### gRPC Client GUI [BloomRPC](https://github.com/bloomrpc/bloomrpc) ### Todo - Research how to upload file via grpc request - How to Debug (Trace Errors) - Testing