fluffydiscord / roadrunner-symfony-bundle
Roadrunner runtime for Symfony
Installs: 2 118
Dependents: 0
Suggesters: 0
Security: 0
Stars: 13
Watchers: 1
Forks: 2
Open Issues: 1
Requires
- php: >=8.3
- nyholm/psr7: ^1.8
- spiral/roadrunner: ^v2024
- spiral/roadrunner-cli: ^v2.6
- spiral/roadrunner-http: ^v3
- spiral/roadrunner-worker: ^v3
- symfony/dependency-injection: ^7.2
- symfony/framework-bundle: ^7.2
- symfony/http-kernel: ^7.2
- symfony/psr-http-message-bridge: ^7.2
- symfony/runtime: ^7.2
Requires (Dev)
- phpunit/phpunit: ^10.5
- roadrunner-php/centrifugo: ^v2
- sentry/sentry-symfony: ^5
- spiral/roadrunner-kv: ^v4
- symfony/mime: ^7.2
README
Yet another runtime for Symfony and RoadRunner.
Installation
composer require fluffydiscord/roadrunner-symfony-bundle
Usage
Define the environment variable APP_RUNTIME
in .rr.yaml
and set up rpc
plugin:
.rr.yaml
server: env: APP_RUNTIME: FluffyDiscord\RoadRunnerBundle\Runtime\Runtime rpc: listen: tcp://127.0.0.1:6001
Don't forget to add the RR_RPC
to your .env
:
RR_RPC=tcp://127.0.0.1:6001
Configuration
fluffy_discord_road_runner.yaml
fluffy_discord_road_runner: # Optional # Specify relative path from "kernel.project_dir" to your RoadRunner config file # if you want to run cache:warmup without having your RoadRunner running in background, # e.g. when building Docker images. Default is ".rr.yaml" rr_config_path: ".rr.yaml" # https://docs.roadrunner.dev/http/http http: # Optional # ----------- # This decides when to boot the Symfony kernel. # # false (default) - before first request (worker takes some time to be ready, but app has consistent response times) # true - once first request arrives (worker is ready immediately, but inconsistent response times due to kernel boot time spikes) # # If you use large amount of workers, you might want to set this to true or else the RR boot up might # take a lot of time or just boot up using only a few "emergency" workers # and then use dynamic worker scaling as described here https://docs.roadrunner.dev/php-worker/scaling lazy_boot: false # Optional # ----------- # This decides if Symfony routing should be preloaded when worker starts and boots Symfony kernel. # This option halves the initial request response time. # (based on a project with over 400 routes and quite a lot of services, YMMW) # # true (default in PROD) - sends one dummy (empty) HTTP request to the kernel to initialize routing and services around it # false (default in DEV) - only when first worker request arrives, routing and services are loaded # # You might want to create a dummy "/" route for the route to "land", # or listen to onKernelRequest events and look in the request for the attribute # FluffyDiscord\RoadRunnerBundle\Worker\HttpWorker::DUMMY_REQUEST_ATTRIBUTE # Set this to "false" if you have any issues and report them to me. early_router_initialization: true # https://docs.roadrunner.dev/plugins/centrifuge centrifugo: # Optional # ----------- # See http section lazy_boot: false # https://docs.roadrunner.dev/key-value/overview-kv kv: # Optional # ----------- # If true (default), bundle will automatically register all "kv" adapters in your .rr.yaml. # Registered services have alias "cache.adapter.rr_kv.NAME" auto_register: true # Optional # ----------- # Which serializer should be used. # By default, "IgbinarySerializer" will be used if "igbinary" php extension # is installed (recommended), otherwise "DefaultSerializer". # You are free to create your own serializer, it needs to implement # Spiral\RoadRunner\KeyValue\Serializer\SerializerInterface serializer: null # Optional # ----------- # Specify relative path from "kernel.project_dir" to a keypair file # for end-to-end encryption. "sodium" php extension is required. # https://docs.roadrunner.dev/key-value/overview-kv#end-to-end-value-encryption keypair_path: bin/keypair.key
Running behind a load balancer or a proxy
If you want to use REMOTE_ADDR
as trusted proxy, replace it with private_ranges
instead
or else your trusted headers will not work.
Symfony is using the $_SERVER['REMOTE_ADDR']
to find out the proxy address,
but in the context of RoadRunner, $_SERVER
contains only environment
variables and the REMOTE_ADDS
is missing.
Response/file streaming
Build-in full support for Symfony's BinaryFileResponse
and StreamedJsonResponse
. The StreamedResponse
needs one little
change to be fully streamable - you have to change the callback
to a \Generator
, replacing all echo
with yield
. Look at the example:
use Symfony\Component\HttpFoundation\StreamedResponse; class MyController { #[Route("/stream")] public function myStreamResponse() { return new StreamedResponse( function () { // replace all 'echo' or any outputs with 'yield' // echo "data"; yield "data"; } ); } }
Sessions
Currently, Symfony might sometimes struggle with sessions in worker mode, like loosing logged user or the opposite, leaking logged user session to another request due to missing globals (explained at the end).
Bundle introduces FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory
, that handles native session correctly.
Register it manually if you happen to run into these issues, for example in framework.yaml
:
framework: session: storage_factory_id: FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory
Sentry
Built in support for Sentry. Just install & configure it as you normally do.
composer require sentry/sentry-symfony
Centrifugo (websockets)
To enable Centrifugo you need to add roadrunner-php/centrifugo
package.
composer require roadrunner-php/centrifugo
Bundle is using Symfony's Event dispatcher. You can create event listener for any event extending FluffyDiscord\RoadRunnerBundle\Event\Centrifugo\CentrifugoEventInterface
:
ConnectEvent
required :)InvalidEvent
PublishEvent
RefreshEvent
RPCEvent
SubRefreshEvent
SubscribeEvent
Example usage:
<?php namespace App\EventListener; use App\Centrifuge\Event\ConnectEvent; use RoadRunner\Centrifugo\Payload\ConnectResponse; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener(event: ConnectEvent::class, method: "handleConnect")] readonly class ChatListener { public function handleConnect(ConnectEvent $event): void { // original Centrifugo request passed from RoadRunner $request = $event->getRequest(); // auth your user or whatever you want $authToken = $request->getData()["authToken"] ?? null; $user = ... // stop propagating to other listeners, // you have successfully connected your user $event->stopPropagation(); // send response using the $event->setResponse($myResponse) $event->setResponse(new ConnectResponse( user: $user->getId(), data: [ "messages" => ... // initial data client receives when connected ], )); } }
Be aware that if you do not set any response, bundle will send DisconnectResponse
back by default.
Developing with Symfony and RoadRunner
- If possible, stop using lazy loading in your services, inject services immediately.
- It is no longer needed and might potentially bring issues to you like memory leaks.
- Do not use/create local class/array caches in your services. Try to make them stateless or if they cannot be, add ResetInterface to clean up before each request.
- Symfony forms might leak data across requests due to local caching it uses. Make sure your form
defaultOptions
are static/immutable. Do not store anything dynamic as it will be cached and used in the following requests. Setting dynamic config values when creating the form type, eg. in controller, is fine. - Simplify your
User
session serialization by taking advantage ofEquatableInterface
and a custom de/serialization logic. This will prevent errors because of detached Doctrine entities and, as a side bonus, will speed up loading user from sessions.
<?php namespace App\Entity\User; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\EquatableInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface { #[ORM\Id] private ?int $id = null; #[ORM\Column(type: Types::TEXT, unique: true)] private ?string $email = null; #[ORM\Column(type: Types::TEXT)] private ?string $password = null; // serialize ony these three fields public function __serialize(): array { return [ "id" => $this->id, "email" => $this->email, "password" => $this->password, ]; } // unserialize ony these three fields public function __unserialize(array $data): void { $this->id = $data["id"] ?? null; $this->email = $data["email"] ?? null; $this->password = $data["password"] ?? null; } // check only the three serialized fields public function isEqualTo(mixed $user): bool { if (!$user instanceof self) { return false; } return $this->id === $user->getId() && $this->password === $user->getPassword() && $this->email === $user->getEmail() ; } }
Debugging (recommendations)
With RoadRunner you cannot simply dump and die, because nothing will be printed. I would like to introduce Buggregator to work around that. As a bonus it can also work as a mailtrap or testing Sentry locally
Credits
Inspiration taken from existing solutions like Baldinof's Bundle and Nyholm's Runtime