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


Comments: