# 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