Revision: Thu, 25 Apr 2024 11:07:00 GMT

GRPC — Service Code

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.

Define the Service

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:

proto
proto/pinger.proto
syntax = "proto3";

option php_namespace = "GRPC\\Pinger";
option php_metadata_namespace = "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.

See more
Make sure to use the options php_namespace and php_metadata_namespace to properly configure PHP namespace. You can read more about the GRPC service declaration here.

Generate the service

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 the protoc-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
app/config/grpc.php
return [
    /**
     * The path where generated DTO (Data Transfer Object) files will be stored.
     */
    'generatedPath' => directory('root') . '/generated',

    /**
     * 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').'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`:
• generated/GRPC/Pinger/PingerInterface.php
• generated/GRPC/Pinger/PingRequest.php
• generated/GRPC/Pinger/PingResponse.php
• generated/GRPC/GPBMetadata/Pinger.php

The code will be generated in the generated/GRPC/Pinger and generated/GRPC/GPBMetadata directories.

And the last step is to register GRPC namespace in the composer.json file:

json
composer.json
{
    ...
    "autoload": {
        "psr-4": {
            "App\\": "app/src",
            "GRPC\\": "generated"
        }
    }
}

and run composer dump-autoload to update the autoloader.

Implement Service

Next, you will need to create a PHP class that implements the Pinger service defined in the .proto file. This class should extend the GRPC/PingerInterface class generated from the .proto file and implement the ping() `method:

php
app/src/Endpoint/GRPC/Pinger.php
namespace App\Endpoint\GRPC;

use Spiral\RoadRunner\GRPC;
use GRPC\Pinger\PingerInterface;
use GRPC\Pinger\PingRequest;
use GRPC\Pinger\PingResponse;

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
        ]);
    }
}

Start the gRPC server:

Make sure to update the proto path in .rr.yaml:

yaml
.rr.yaml
grpc:
  listen: tcp://0.0.0.0:9001
  proto:
    - "proto/pinger.proto"
./rr serve

Multiple Services

Use the import directive of proto declarations to combine multiple services in a single application, or store message declarations separately.

Metadata

Use Spiral\GRPC\ContextInterface to access request metadata. There are a number of system metadata properties you can read:

php
use Spiral\RoadRunner\GRPC;
use GRPC\Pinger\PingRequest;
use GRPC\Pinger\PingResponse;

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()
    ]);
}

See more
Read more about auth practices here.

Response Headers

You can add any custom metadata to the response using Context-specific response headers:

php
use Spiral\RoadRunner\GRPC;
use GRPC\Pinger\PingRequest;
use GRPC\Pinger\PingResponse;

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()
    ]);
}

Errors

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)

See more
See all status codes in Spiral\RoadRunner\GRPC\StatusCode. Read more about GRPC error codes here.

Example:

php
use Spiral\RoadRunner\GRPC;
use GRPC\Pinger\PingRequest;
use GRPC\Pinger\PingResponse;

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(),
    ]);
}

Best Practices

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:

  • Image-SDK - v1.2.0

Services:

  • Image-Service - implements Image-SDK v1.2.0
  • Account-Service - requires Image-SDK v1.1.0