Helps implementing Twirp in a Symfony application.
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.
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.
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);
}