Introduction to Symfony Microservice architecture with gRPC communication
Introduction
Hello, In this article, we will explore Symfony Microservice architecture and gRPC communication. For those unfamiliar with gRPC, we’ll start with a brief introduction. Readers already familiar with gRPC topic may skip this section.
gRPC: gRPC is a high-performance, open-source remote procedure call (RPC) framework developed by Google. It enables efficient and seamless communication between applications written in different languages.
After providing this foundational knowledge, we will dive into creating a Microservice architecture with Symfony and examine how to establish communication between these services using gRPC.
First of all, PHP doesn’t have any gRPC server implementation by default. If we look at to official documentation of gRPC we can see that only a client for gRPC can be created in PHP language.
So we must first solve the server issue and this is exactly where Roadrunner comes to our rescue.
Roadrunner
Roadrunner is a high-performance application server, load balancer, and PHP process manager written in Go. By operating as an HTTP or gRPC server, it forwards requests to a pool of constantly running PHP processes via the Goridge binary protocol. This design keeps PHP workers alive, eliminating the need to restart them with each request — unlike the traditional Nginx + PHP-FPM setup.
Requirements
- Composer for creating symfony projects
- Docker for EVERYTHING else we need :)
.proto File
A .proto file is a way to define how data is structured and how services are called when using Protocol Buffers (Protobuf). Essentially, it describes:
1. What data we want to send (the messages).
2. What operations we can perform (the services and their methods).
First of all, we must create a .proto file which will define our contract between Client and Product services.
service.proto:
syntax = "proto3";
package grpc.ProductService;
service ClientProductService {
rpc GetProductsByClient(ClientRequest) returns (ProductResponse);
}
message ClientRequest {
string client_id = 1;
}
message Product {
string client_id = 1;
string product_id = 2;
string name = 3;
string description = 4;
double price = 5;
}
message ProductResponse {
repeated Product products = 1;
}
Below is a step-by-step explanation of each element in our .proto file:
1. Syntax = “proto3”;
• Indicates we’re using Protocol Buffers version 3 (the modern version).
2. Package grpc.ProductService;
• Groups everything into a namespace called grpc.ProductService.
3. Service: ClientProductService
• Defines one remote procedure call, GetProductsByClient, which takes a ClientRequest and returns a ProductResponse.
4. Message: ClientRequest
• Has a single field, client_id, which specifies the client whose products we want.
5. Message: Product
• Represents an individual product with fields for client_id, product_id, name, description, and price.
6. Message: ProductResponse
• Contains a repeated Product products field — a list of Product items to return for the request.
In short:
• ClientRequest says, “Here is the client_id. I need the products belonging to this client.”
• GetProductsByClient says, “Given a ClientRequest, I’ll return a ProductResponse containing all the products for that client.”
• ProductResponse says, “Here’s a list of Product objects, each with client_id, product_id, name, description, and price.”
Creating Symfony projects
First create a main directory which will contain our services.
mkdir symfony-microservices-example
cd symfony-microservices-example
Then create our services.
composer create-project symfony/skeleton ProductService
composer create-project symfony/skeleton ClientService
At this point we will have 2 projects named ProductService
and ClientService
In this project we are going to use Docker for our needs. So create a directory for our base Dockerfile.
cd symfony-microservices-example
mkdir BaseDocker
cd BaseDocker
touch Dockerfile
Base Dockerfile:
FROM php:8.3-cli-alpine
WORKDIR /app
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
RUN install-php-extensions bcmath intl opcache zip sockets grpc protobuf
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
linux-headers \
&& mkdir -p /tmp/pear/cache \
&& apk add --update --no-cache \
openssl-dev \
cmake \
libunwind \
libunwind-dev \
pcre-dev \
icu-dev \
icu-data-full \
libzip-dev \
git \
go \
&& mkdir /build && cd /build \
&& git clone --recursive -b v1.27.x https://github.com/grpc/grpc \
&& mkdir -p /build/grpc/cmake/build && cd /build/grpc/cmake/build \
&& cmake ../.. \
&& make protoc grpc_php_plugin \
&& cd /build \
&& composer create-project --ignore-platform-reqs spiral/roadrunner-cli \
&& chmod +x ./roadrunner-cli/bin/rr \
&& ./roadrunner-cli/bin/rr download-protoc-binary -l /usr/bin \
&& cp /build/grpc/cmake/build/grpc_php_plugin /usr/bin \
&& cp /build/grpc/cmake/build/third_party/protobuf/protoc /usr/bin \
&& chmod +x /usr/bin/protoc-gen-php-grpc \
&& chmod +x /usr/bin/grpc_php_plugin \
&& chmod +x /usr/bin/protoc \
&& rm -rf /build \
&& pecl clear-cache \
&& apk del --purge .build-deps
Let’s build this image with the below command (it can take up to 10 minutes to build):
docker build -t base-php8.3-with-grpc:latest .
If you don’t have time to build your own image you can use my pre-build image from: https://hub.docker.com/r/oguzhankrcb/base-php8.3-with-grpc
And then we will create our docker-compose.yaml
:
services:
service_a:
build:
context: ./ProductService
ports:
- "8084:8084"
volumes:
- ./ProductService:/app/
networks:
- app-network
service_b:
build:
context: ./ClientService
ports:
- "8085:8085"
volumes:
- ./ClientService:/app/
networks:
- app-network
networks:
app-network:
driver: bridge
We will need specific Dockerfiles for our both ProductService
and ClientService
projects.
ClientService Dockerfile:
FROM base-php8.3-with-grpc:latest
WORKDIR /app
COPY . .
CMD ["php", "-S", "0.0.0.0:8085", "-t", "public/"]
ProductService Dockerfile:
FROM base-php8.3-with-grpc:latest
WORKDIR /app
COPY . .
CMD ["php", "-S", "0.0.0.0:8084", "-t", "public/"]
Finally we can build our compose file:
docker compose build --no-cache
Installing roadrunner-bundle
The baldinof/roadrunner-bundle package streamlines integration with RoadRunner in Symfony by providing a pre-configured worker, various settings, and configuration options. It simplifies the process of getting started with Symfony and RoadRunner together. To install the package, run the following command and confirm the recipe installation (y):
docker compose run service_a composer require baldinof/roadrunner-bundle
Once installed, the bundle will be automatically registered, and the necessary configuration files will be generated.
We will update the RoadRunner configuration files, .rr.yaml and .rr.dev.yaml, as follows:
1. Remove the unnecessary http plugin:
The http section, which is no longer required, will be removed. Here’s an example of the section to delete:
http:
address: 0.0.0.0:8080
...
2. Add the grpc section:
We’ll replace the removed http section with a configuration for the gRPC plugin. The updated configuration will look like this:
grpc:
listen: "tcp://0.0.0.0:8084"
proto:
- "proto/service.proto"
Don’t forge to move our previously written service.proto
file to ./ProductService/proto/service.proto
.
Then let’s run this command to install grpc/grpc
and spiral/roadrunner-grpc
packages:
docker compose run service_a composer require grpc/grpc spiral/roadrunner-grpc
Then we must download roadrunner binary in order to run roadrunner server:
docker compose run service_a composer require --dev spiral/roadrunner-cli
docker compose run service_a ./vendor/bin/rr get-binary -l ./bin
cd ProductService/bin
chmod u+x rr
Generating gRPC codes
To generate PHP code for the gRPC server and client, we will use protoc, the utility responsible for code generation. This requires a plugin for PHP gRPC code generation, specifically protoc-gen-php-grpc.
• Server Code Generation:
For the server, we’ll use the protoc-gen-php-grpc plugin provided by the RoadRunner project. However, this plugin does not generate client classes.
• Client Code Generation:
To generate client classes, we’ll use the official grpc_php_plugin. Unfortunately, this plugin must be manually built from source, which can involve resolving dependencies and may be time-consuming.
To simplify the process, I’ve prepared a Docker image that builds protoc, grpc_php_plugin, and protoc-gen-php-grpc for you. Using this image eliminates the need for manual setup. It handles the entire process automatically.
Steps to Generate the Code:
1. Create a Directory for the Generated Code:
We’ll store the generated code in a directory named generated. Create the directory with:
mkdir ./generated
2. Run the Code Generation Command:
Both server and client code can be generated using the same command, differing only in the — plugin argument.
With this setup, you’ll have a seamless process for generating both server and client code in the generated directory.
Server Code (ProductService)
We will generate our server stubs with the protoc-gen-php-grpc
plugin:
docker compose run service_a protoc \
--plugin=protoc-gen-grpc=/usr/bin/protoc-gen-php-grpc \
--php_out=./generated \
--grpc_out=./generated \
./proto/service.proto
We must see the above files in our project.
Implementing ProductService Class
We are going to develop our gRPC request handler
class, let’s create ProductService/src/Services/ProductService.php
file:
<?php
namespace App\Services;
use Faker\Factory;
use Faker\Generator;
use Grpc\ProductService\ClientProductServiceInterface;
use Grpc\ProductService\ClientRequest;
use Grpc\ProductService\Product;
use Grpc\ProductService\ProductResponse;
use Spiral\RoadRunner\GRPC;
class ProductService implements ClientProductServiceInterface
{
protected Generator $faker;
public function __construct()
{
$this->faker = Factory::create();
}
public function GetProductsByClient(GRPC\ContextInterface $ctx, ClientRequest $in): ProductResponse
{
$productResponse = new ProductResponse();
$productResponse->setProducts($this->getRandomProducts($in->getClientId()));
return $productResponse;
}
private function getRandomProducts(string $clientId): array
{
$products = [];
$randomLength = random_int(3, 5);
for ($i = 0; $i < $randomLength; $i++) {
$product = (new Product())->setProductId($i + 1)
->setClientId($clientId)
->setName($this->faker->domainName)
->setDescription($this->faker->word)
->setPrice($this->faker->numberBetween(100,1000));
$products[] = $product;
}
return $products;
}
}
Don’t forget to add faker
package:
docker compose run service_a composer require fzaninotto/faker --dev
Now we are going to refactor our Dockerfile to start our gRPC server with roadrunner:
FROM base-php8.3-with-grpc:latest
WORKDIR /app
COPY . .
CMD ["/app/bin/rr", "serve", "-d", "-c", "/app/.rr.yaml"]
Then to autoload generated files, we must add the following lines to the composer.json:
...
"autoload": {
"psr-4": {
...
"GPBMetadata\\": "generated/GPBMetadata",
"Grpc\\": "generated/Grpc"
}
},
...
After that we must run composer dump-autoload:
docker compose run service_a composer du
Then let’s build and run our compose file:
docker compose build --no-cache
docker compose up
If you did everything right then you must see a screen like this:
Now we can test our server with grpcurl
tool or something like that, i will use grpcurl
if you use macOS you can download it with brew
:
brew install grpcurl
And run the below command to test:
grpcurl -plaintext -d '{"client_id":"123456"}' -import-path ./proto -proto service.proto localhost:8084 grpc.ProductService.ClientProductService/GetProductsByClient
You must saw a response like this:
Client Code (ClientService)
First of all we must install grpc/grpc
package to use grpc client
:
docker compose run service_b composer req grpc/grpc
Then copy our service.proto file to ClientService/proto/service.proto
and create generated
directory as ClientService/generated
and run the command:
docker compose run service_b protoc \ ✔ │ 3m 51s │ system │ 17:01:13
--plugin=protoc-gen-grpc=/usr/bin/grpc_php_plugin \
--php_out=./generated \
--grpc_out=./generated \
./proto/service.proto
And the below files must be created:
Then you must enable ext-grpc
extension from your composer.json:
...
"require": {
...
"ext-grpc": "*",
...
},
...
Implementing ClientProductController
Now we are going to develop our ClientProductController
, so create a file to src/Controller/ClientProductController.php
:
<?php
declare(strict_types=1);
namespace App\Controller;
use Grpc\ChannelCredentials;
use Grpc\ProductService\ClientProductServiceClient;
use Grpc\ProductService\ClientRequest;
use Grpc\ProductService\ProductResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class ClientProductController extends AbstractController
{
#[Route('/clients/{client_id}/products', name: 'app_client_products', methods: ['GET'])]
public function clientProducts(string $client_id): Response
{
$productClient = new ClientProductServiceClient('service_a:8084', [
'credentials' => ChannelCredentials::createInsecure(),
]);
$productRequest = (new ClientRequest())->setClientId($client_id);
/** @var ProductResponse $response */
[$response] = $productClient->GetProductsByClient($productRequest)->wait();
return JsonResponse::fromJsonString($response->serializeToJsonString());
}
}
Then don’t forget to add our namespaces to composer.json:
...
"autoload": {
"psr-4": {
...
"GPBMetadata\\": "generated/GPBMetadata",
"Grpc\\": "generated/Grpc"
}
},
...
Run below command to dump-autoload:
docker compose run service_b composer du
Finally we can up our compose file:
docker compose up
If we did everything right we can go to that URL:
http://localhost:8085/clients/1234/products
And when we open this URL we will see a response like this:
Conclusion
In this article, we explored the process of setting up a Symfony-based microservice architecture using gRPC communication and learned that how combining Symfony’s flexibility with gRPC’s efficiency can create a robust foundation for scalable, high-performance microservices.
Whether you’re building an enterprise application or experimenting with modern architectures, this approach offers a practical roadmap for PHP developers to harness the potential of gRPC communication. With this knowledge, you’re equipped to design and implement your own gRPC-powered microservice solutions in Symfony.
You can find the ready-to-go source code of this project from my github
https://github.com/oguzhankrcb/symfony-micro-service-with-grpc