fluffydiscord / roadrunner-symfony-bundle
Roadrunner runtime for Symfony
Requires
- php: >=8.2
- nyholm/psr7: ^1.8
- spiral/roadrunner: ^v2024.1
- spiral/roadrunner-cli: ^v2.6
- spiral/roadrunner-http: ^v3.5
- spiral/roadrunner-worker: ^v3.5
- symfony/dependency-injection: ^7.0
- symfony/framework-bundle: ^7.0
- symfony/http-kernel: ^7.0
- symfony/psr-http-message-bridge: ^7.0
- symfony/runtime: ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5
- roadrunner-php/centrifugo: ^v2.1
- sentry/sentry-symfony: ^4.13
- spiral/roadrunner-kv: ^v4.1
- symfony/mime: ^7.0
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 # 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 0.0.0.0/0
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"; } ); } }
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 stateless. Do not store anything sensitive/important as it will be leaked in the following requests. - Simplify your
User
session serialization by taking advantage ofEquatableInterface
and 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