gRPC Server and Client with Symfony
Introduction
This is a demonstration on how to use a gRPC client to request data from a gRPC server.
You’ll need protoc and grpc_php_plugin, you can download them following this section https://grpc.io/docs/languages/php/basics/#setup.
gRPC Server
First, we’ll configure the gRPC Server. To do this, create a new Symfony project:
symfony new grpc_server --version="lts"
Next, install Roadrunner, which is the application server we’re using instead of FPM + nginx/Apache.
cd grpc_server
composer require baldinof/roadrunner-bundle
composer require --dev spiral/roadrunner-cli
vendor/bin/rr get --location bin/
composer require spiral/roadrunner-grpc
Then, run the following command to get protoc-gen-php-grpc, we’ll use it
to generate the gRPC files:
./vendor/bin/rr download-protoc-binary
Also register the GRPC namespace in composer.json:
"psr-4": {
"App\\": "src/",
"GRPC\\": "GRPC/"
}
Create the catalog.proto file inside src/proto folder:
syntax = "proto3";
option php_namespace = "GRPC\\Catalog";
option php_metadata_namespace = "GRPC\\GPBMetadata";
package CatalogApi;
message CatalogItemRequest {
int32 id = 1;
}
message CatalogItemResponse {
int32 id = 1;
string name = 2;
string description = 3;
double price = 4;
}
service Catalog {
rpc GetItemById (CatalogItemRequest) returns (CatalogItemResponse) {
}
}
Enable the gRPC port in .rr.yaml and .rr.dev.yaml:
grpc:
listen: "tcp://0.0.0.0:50000"
proto:
- "src/proto/catalog.proto"
Now, generate the gRPC files:
protoc --plugin=protoc-gen-php-grpc \
--php_out=./ \
--php-grpc_out=./ \
src/proto/catalog.proto
The files were generated in GRPC/ folder, we should implement the CatalogInterface.
In src/GRPC/, create a file CatalogService.php and copy this:
<?php
namespace App\GRPC;
use GRPC\Catalog\CatalogInterface;
use GRPC\Catalog\CatalogItemRequest;
use GRPC\Catalog\CatalogItemResponse;
use Psr\Log\LoggerInterface;
use Spiral\RoadRunner\GRPC;
class CatalogService implements CatalogInterface
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function GetItemById(GRPC\ContextInterface $ctx, CatalogItemRequest $in): CatalogItemResponse
{
$this->logger->info("Get item by id {$in->getId()}");
if ($in->getId() <= 0) {
throw new GRPC\Exception\GRPCException(
"ID should be greater than 0",
GRPC\StatusCode::FAILED_PRECONDITION
);
}
return (new CatalogItemResponse())
->setId($in->getId())
->setName("Shoes")
->setPrice(400)
->setDescription("The best shoes!");
}
}
Start the server:
bin/rr serve -c .rr.dev.yaml --debug
The server is running and listening at the port 8080 (http) and 50000 (gRPC).
gRPC Client
Start a new project and install roadrunner
symfony new grpc_client --version="lts"
cd grpc_client
composer require baldinof/roadrunner-bundle
composer require --dev spiral/roadrunner-cli
vendor/bin/rr get --location bin/
Download protoc-gen-php-grpc
./vendor/bin/rr download-protoc-binary
Update composer.json:
"psr-4": {
"App\\": "src/",
"GRPC\\": "GRPC/"
}
Create catalog.proto in src/proto/
syntax = "proto3";
option php_namespace = "GRPC\\Catalog";
option php_metadata_namespace = "GRPC\\GPBMetadata";
package CatalogApi;
message CatalogItemRequest {
int32 id = 1;
}
message CatalogItemResponse {
int32 id = 1;
string name = 2;
string description = 3;
double price = 4;
}
service Catalog {
rpc GetItemById (CatalogItemRequest) returns (CatalogItemResponse) {
}
}
Add the following packages related to gRPC, and serializer, to convert the response from the gRPC server into an array:
composer require grpc/grpc ext-grpc serializer
Generate the gRPC files
protoc --plugin=protoc-gen-php-grpc=grpc_php_plugin \
--php_out=./ \
--php-grpc_out=./ \
src/proto/catalog.proto
We can add the service in config/services.yaml to pass the arguments hostname and credentials.
We are using the hostname from an environment variable, and the second argument is an array with credentials equals to null
(which is equivalent of pass ChannelCredentials::createInsecure() according to the definition of that method).
services:
# ...
GRPC\Catalog\CatalogClient:
arguments:
- '%grpc_catalog_url%'
- { credentials: null }
Let’s define grpc_catalog_url, go to the top of that file and add this to the parameters:
parameters:
# ...
grpc_catalog_url: '%env(GRPC_CATALOG_URL)%'
Now add an environment variable called GRPC_CATALOG_URL in the .env file:
GRPC_CATALOG_URL=127.0.0.1:50000
Create a BasketController.php in src/Controller/:
<?php
namespace App\Controller;
use GRPC\Catalog\CatalogClient;
use GRPC\Catalog\CatalogItemRequest;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
class BasketController extends AbstractController
{
private CatalogClient $catalogClient;
private SerializerInterface $serializer;
public function __construct(CatalogClient $catalogClient, SerializerInterface $serializer)
{
$this->catalogClient = $catalogClient;
$this->serializer = $serializer;
}
#[Route('/api/basket/items', name: 'basket', methods: ['POST'])]
public function addBasketItem(Request $request): JsonResponse
{
$request = json_decode($request->getContent(), true);
$response = $this->catalogClient->GetItemById((new CatalogItemRequest())->setId($request['itemId']));
return new JsonResponse($this->serializer->normalize($response->wait()[0]));
}
}
Now, before running the server, update the .rr.yml and .rr.dev.yml to use a different HTTP port:
http:
address: 0.0.0.0:8081
Run it:
bin/rr serve -c .rr.dev.yaml --debug
Now, test if it’s working:
curl -X POST 'localhost:8081/api/basket/items' \
-H 'Content-Type: application/json' \
-d '{ "itemId": 50 }'
Full solution at: https://github.com/hexdump95/grpc_symfony