timostamm / symfony-twirp-handler
Helps implementing Twirp in a Symfony application
Installs: 301
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 1
Open Issues: 0
pkg:composer/timostamm/symfony-twirp-handler
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); }