timostamm / symfony-twirp-handler
Helps implementing Twirp in a Symfony application
Requires
- php: >=7.1
- google/protobuf: ^3.10
- psr/container: ^1.0
- symfony/event-dispatcher: >=4
- symfony/http-foundation: >=4.3
- symfony/http-kernel: >=4
- symfony/property-info: >=4
Requires (Dev)
- phpunit/phpunit: ^9.5
Suggests
- ext-bcmath: Need to support JSON deserialization
- psr/container: To resolve service implementation from container
- psr/log: To handle requests
- symfony/http-foundation: To handle requests
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); }