zolex/twirp-bundle

Twirp Bundle for Symfony

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 0

Open Issues: 0

Type:symfony-bundle

0.0.1 2025-04-20 22:45 UTC

This package is auto-updated.

Last update: 2025-04-20 22:51:46 UTC


README

Release Version Integration Code Coverage

License Downloads

TwirpBundle

PHP Symfony

A Symfony Bundle providing an implementation of a Twirp PHP Server.

Configuration

Autoloading

The classes generated by protoc-gen-twirp_symfony must be included in compsoer.json

{
  "autoloading": {
    "psr-4": {
      "Zolex\\RPC\\": "twirp-php/src/Zolex/RPC/"
    }
  }
}

Symfony Services

Additionally, the generated classes must be added as symfony services, for example via config/services.yml

services:
  Zolex\RPC\:
    resource: '../twirp-php/src/Zolex/RPC/'

Routing

To enable routing for the symfony bundle, simply add this files to your project config/routes/twirp.yaml

twirp:
  type: twirp
  resource: .

Static routes for each method in each twirp service are generated automatically. You can view the available routes by typing bin/console debug:router.

Service Prefix

A custom prefix should be defined for each service in the project's bundle config. The key in the services section must match the fully qualified classname of your service implementation.

config/packages/zolex_twirp.yaml

zolex_twirp:
  services:
    App\Twirp\ExampleService:
      prefix: '/examples.twirphp-server'

Generate the code

First you need to install protoc and the protoc-gen-twirp_symfony plugin and of course you need a protobuf service definition.

These are the aditional protoc arguments you have to specify for symfony.

--plugin=protoc-gen-twirp_symfony=/usr/local/bin/protoc-gen-twirp_symfony \
--php_out=path/to/twirp-php \
--twirp_symfony_out=path/to/twirp-php

NOTE: See autoloading/services section for path/to/twirp-php

Implement a Service

The generatedd code from the twirp_symfony plugin includes Interfaces that you have to implement and register your implementation as a Symfony service (with symfony/framework-bundle just place it in your app folder, that's all it needs!)

class ExampleService implements ExampleServiceInterface
{
    public function doSomething(array $ctx, InputMessage $input): OutputMessage
    {
        return new OutputMessage(['content' => 'It\'s that easy!']);
    }
}

Of course your service is registered for Dependency Injection so just typehint any symfony service that you need.

Events

This bundle is designed with an Event Driven Architecture utilizing Symfony standards. We explicitly do no implement the hooks- and interceptors feature as described by Twirp. Instead of a Twirp ServerHook you are going to write EventListeners or EventSubscribers, hooking into the Symfony kernel events.

Every Service that generated by protoc-gen-twirp_symfony and that you implement in your project is automatically registered as a ServiceListener.

Inlcuded Listeners

Event Priority Listener Task
kernel.request 10 ServiceListener Creates an instance of a Twirp Input Message
kernel.request 6 DeserializeListener Deseriaizes the Rrequest Body into the Twirp Message
kernel.view 10 ServiceListener Handles the request by calling the Twirp Service Method
kernel.view 6 SerializeListener Serializes the output of the service method to JSON or protobuf
kernel.exception 100 ExceptionListener Converts Exceptions to TwirpErrors and sends the error response

Utilizing the following constants makes it easy to hook in your custom listeners or subscribers at right place:

class EventPriorities
{
    // kernel.request
    public const PRE_INPUT_FACTORY = 11;
    public const POST_INPUT_FACTORY = 9;
    public const PRE_DESERIALIZE = 7;
    public const POST_DESERIALIZE = 5;

    // kernel.view
    public const PRE_HANDLE_REQUEST = 11;
    public const POST_HANDLE_REQUEST = 9;
    public const PRE_SERIALIZE = 7;
    public const POST_SERIALIZE = 5;

    // kernel.terminate
    public const POST_SEND_RESPONSE = 0;
}

Implement a custom listener or subscriber

On top of the Event there are plenty of options how you can determine if your listener should run. It's recommened to make the decision using the folowing request attributes and implement the Early Return Pattern.

  • input: The twirp input message (when in the REQUEST event)
  • _route: The name of the called route (see routing section)
  • _twirp_context: The twirp context
  • _twirp_prefix: Twirp path prefix (see configuration section)
  • _twirp_package: Twirp package name (from the protobuf definition)
  • _twirp_service: Twirp service name (e.g. ExampleService)
  • _twirp_method: The twirp method to be calledd (e.g. doSomething)

Note: In the VIEW event you should use $event->getControllerResult() to retrieve the input message before the Reuqest was handled by ServiceListener and the output message afterwards.

Here is an example on how to inject a value to the twirp context for a specific twirp service method.

class AddToContextSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::VIEW => ['onKernelView', EventPriorities::PRE_HANDLE_REQUEST]
        ];
    }

    public function onKernelView(ViewEvent $event): void
    {
        $request = $event->getRequest();
        $route = $request->attributes->get('_route');
        if ('twirp:example_service:some_method' !== $route) {
            return;
        }

        $ctx = $request->attributes->get('_twirp_context');
        $ctx['some_new_context'] = 1337;
        $request->attributes->set('_twirp_context', $ctx);
    }

Examples

Have a look at the example server project using this bundle.