timostamm/symfony-twirp-handler

Helps implementing Twirp in a Symfony application

v0.0.5 2022-12-12 08:55 UTC

This package is auto-updated.

Last update: 2024-12-02 10:33:48 UTC


README

Helps implementing Twirp in a Symfony application.

Most simple way

Lets say you have this service defined in a proto file:

syntax = "proto3";
service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse);
}

First you generate PHP code with protoc, for example:

protoc --proto_path protos/ --php_out out-php protos/example-service.proto

Then you create a controller for the corresponding Twirp routes:

// SearchServiceController.php
/**
 * @Route("/twirp/SearchService")
 */
class SearchServiceController
{
    
    use TwirpControllerTrait;

    /**
     * @Route("/MakeHat")
     */
    public function makeHat(Request $request): Response
    {
        /** @var SearchRequest $input */
        $input = $this->readTwirp($request, SearchRequest::class);
        // ...
        
        $output = new SearchResponse();
        // ...

        return $this->writeTwirp($request, $output);
    }

}

Twirp route URLs are constructed like this: twirp/{proto package name}.{proto service name}/{proto method name}.

readTwirp() and writeTwirp() will do content negotiation for you.

Supporting errors properly

Twirp has its own error format. To convert exceptions automatically, you can use the TwirpErrorSubscriber:

// services.yaml
SymfonyTwirp\TwirpErrorSubscriber:
    arguments:
        $requestTagAttribute: "_request_id"
        $debug: '%kernel.debug%'
        $prefix: "twirp"

If you have this subscriber set up, you can also throw your own TwirpError (with full control over twirp error code and meta data). For documentation about the arguments of TwirpErrorSubscriber, check the PHPdoc.

Advanced use

Writing symfony routes for every RPC is tedious and error-prone.
Enable php_generic_services to generate a PHP interface for each service:

// example-service.proto
syntax = "proto3";

option php_generic_services = true;

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse);
}

From this file, protoc generates a generic service interface SearchServiceInterface.php. Create a new class SearchService and implement the interface:

// SearchService.php
class SearchService implements SearchServiceInterface
{

    public function search(SearchRequest $request)
    {
        $response = new SearchResponse();
        $response->setHits(['a', 'b', 'c']);
        return $response;
    }

}

To serve this service via Twirp, you can use the TwirpHandler. You only need a single route for one or more services. The handler takes care of the routing, content negotiation, parsing and serializing and automatically invokes the correct method on your service.

Create a route that matches all twirp/ requests and use the TwirpHandler as follows:

// TwirpController.php
/**
 * @Route( path="twirp/{serviceName}/{methodName}" )
 */
public function execute(RequestInterface $request, string $serviceName, string $methodName): Response
{
    $resolver = new ServiceResolver();
    
    $resolver->registerInstance(
        SearchServiceInterface::class, // the interface generated by protoc 
        new SearchService() // your implementation of the interface
    );
    
    // alternatively, you can register a factory
    // $resolver->registerFactory(SearchServiceInterface::class, function() {
    //    return new SearchService();
    // });
    
    // .. or a PSR container with $resolver->registerContainer()

    $handler = new TwirpHandler($resolver);

    return $handler->handle($serviceName, $methodName, $request);
}