Using gRPC can be simpler than building a REST API in some cases, because it provides a more structured and efficient way to define and communicate between APIs.
Here are some benefits of using gRPC compared to REST APIs:
Strongly-typed interfaces: In gRPC, the service and message types are defined in a .proto
file, which allows for
the creation of strongly-typed interfaces. This can make it easier to ensure that the correct data is being passed
between the client and server, as the types are checked at compile-time.
Efficient binary encoding: gRPC uses Protocol Buffers, a binary encoding format, to serialize and transmit data. This can be more efficient than JSON, which is the common encoding format for REST APIs, as it requires fewer bytes to transmit the same data.
Language and platform agnostic: gRPC uses a universal .proto
file to define the service and message types, which
can be used to generate code in multiple languages and platforms. This allows for the creation of cross-platform APIs
that can be used by clients written in any language.
Note
Use https://github.com/spiral/ticket-booking as the base to speed up onboarding.
To declare our first service, create a proto file in the desired direction. By default, the GRPC build suggests creating
proto files in the/proto
directory. Create a file proto/pinger.proto
:
syntax = "proto3";
option php_namespace = "App\\GRPC\Pinger";
option php_metadata_namespace = "App\\GRPC\\GPBMetadata";
package pinger;
service Pinger {
rpc ping (PingRequest) returns (PingResponse) {
}
}
message PingRequest {
string url = 1;
}
message PingResponse {
int32 status_code = 1;
}
This .proto
file defines a service called Pinger with a single method, ping
, which takes a PingRequest
message
as input and returns a PingResponse
message.
Note
Make sure to use the options
php_namespace
andphp_metadata_namespace
to properly configure PHP namespace. You can read more about the GRPC service declaration here.
At the moment you can only create Unidirectional APIs.
To compile the .proto
file into PHP code, you will need to use the protoc
compiler and the protoc-gen-php-grpc
.
Note
Here you can find the installation instructions for the
grpc
PHP extension,protoc
compiler and theprotoc-gen-php-grpc
plugin.
At first, you need to generate the service stubs. To do this, you need to add them to the app/config/grpc.php
configuration file:
<?php
declare(strict_types=1);
return [
/**
* The path where generated DTO (Data Transfer Object) files will be stored.
*/
'generatedPath' => directory('app') . '/GRPC',
/**
* The base namespace for the generated proto files.
*/
'namespace' => '\App\GRPC',
/**
* The root dir for all proto files, where imports will be searched.
*/
'servicesBasePath' => directory('root') . '/proto',
/**
* The path to the protoc-gen-php-grpc library.
*/
'binaryPath' => directory('root').'/bin/protoc-gen-php-grpc',
/**
* An array of paths to proto files that should be compiled into PHP by the grpc:generate console command.
*/
'services' => [
directory('root').'proto/pinger.proto',
],
];
Then, you can compile the pinger.proto
file using the following command:
php app.php grpc:generate
You should see the following output:
Compiling `proto/pinger.proto`:
• app/src/GRPC/Pinger/PingerInterface.php
• app/src/GRPC/Pinger/PingRequest.php
• app/src/GRPC/Pinger/PingResponse.php
• app/src/GRPC/GPBMetadata/Pinger.php
The code will be generated in the app/src/App/GRPC/Pinger
and app/src/App/GRPC/GPBMetadata
directories.
Next, you will need to create a PHP class that implements the Pinger service defined in the .proto
file. This
class should extend the app/src/GRPC/PingerInterface
class generated from the .proto
file and implement the
ping()
`method:
<?php
declare(strict_types=1);
namespace App\GRPC\Pinger;
use Spiral\RoadRunner\GRPC;
final class Pinger implements PingerInterface
{
public function __construct(
private readonly HttpClientInterface $httpClient
) {
}
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
$statusCode = $this->httpClient->get($in->getUrl())->getStatusCode();
return new PingResponse([
'status_code' => $statusCode
]);
}
}
Make sure to update the proto path in .rr.yaml
:
grpc:
listen: tcp://0.0.0.0:9001
proto:
- "proto/pinger.proto"
./rr serve
Use the import
directive of proto declarations to combine multiple services in a single application, or store message
declarations separately.
Use Spiral\GRPC\ContextInterface
to access request metadata. There are a number of system metadata properties you
can read:
use Spiral\RoadRunner\GRPC;
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
dump($ctx->getValue(':authority'));
dump($ctx->getValue(':peer.address'));
dump($ctx->getValue(':peer.auth-type'));
dump($ctx->getValue('user-agent'));
dump($ctx->getValue('content-type'));
return new PingResponse([
'status_code' => $this->httpClient->get($in->getUrl())->getStatusCode()
]);
}
Note
Read more about auth practices here.
You can add any custom metadata to the response using Context-specific response headers:
use Spiral\RoadRunner\GRPC;
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
/** @var GRPC\ResponseHeaders $responseHeaders */
$responseHeaders = $ctx->getValue(GRPC\ResponseHeaders::class);
$responseHeaders->set('key', 'value');
return new PingResponse([
'status_code' => $this->httpClient->get($in->getUrl())->getStatusCode()
]);
}
The spiral/roadrunner-grpc
component provides a number of exceptions to indicate a server or request error:
Exception | Error Code |
---|---|
Spiral\RoadRunner\GRPC\Exception\GRPCException | UNKNOWN(2) |
Spiral\RoadRunner\GRPC\Exception\InvokeException | UNAVAILABLE(14) |
Spiral\RoadRunner\GRPC\Exception\NotFoundException | NOT_FOUND(5) |
Spiral\RoadRunner\GRPC\Exception\ServiceException | INTERNAL(13) |
Spiral\RoadRunner\GRPC\Exception\UnauthenticatedException | UNAUTHENTICATED(16) |
Spiral\RoadRunner\GRPC\Exception\UnimplementedException | UNIMPLEMENTED(12) |
Note
See all status codes in
Spiral\RoadRunner\GRPC\StatusCode
. Read more about GRPC error codes here.
use Spiral\RoadRunner\GRPC;
public function ping(GRPC\ContextInterface $ctx, PingRequest $in): PingResponse
{
if ($in->getUrl() === '') {
throw new GRPC\ServiceException('URL is empty');
}
if (!\filter_var($url, FILTER_VALIDATE_URL)) {
throw new GRPC\ServiceException(\sprintf('URL "%s" is invalid', $url));
}
return new PingResponse([
'status_code' => $this->httpClient->get($in->getUrl())->getStatusCode(),
]);
}
The recommended approach for designing the GRPC API for a spiral application is to generate service code interfaces, messages, and client code in a separate repository.
Common:
Services: