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