fluffydiscord / roadrunner-symfony-bundle
Symfony 的 Roadrunner 运行时
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
为 Symfony 和 RoadRunner 提供另一个运行时。
安装
composer require fluffydiscord/roadrunner-symfony-bundle
用法
在 .rr.yaml
中定义环境变量 APP_RUNTIME
并设置 rpc
插件
.rr.yaml
server: env: APP_RUNTIME: FluffyDiscord\RoadRunnerBundle\Runtime\Runtime rpc: listen: tcp://127.0.0.1:6001
别忘了将 RR_RPC
添加到你的 .env
RR_RPC=tcp://127.0.0.1:6001
配置
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
位于负载均衡器或代理后面运行
如果你想将 REMOTE_ADDR
作为受信任的代理使用,请将其替换为 0.0.0.0/0
,否则你的受信任头将无法工作。
Symfony 正在使用 $_SERVER['REMOTE_ADDR']
来查找代理地址,但在 RoadRunner 的上下文中,$_SERVER
只包含环境变量,而 REMOTE_ADDS
是缺失的。
响应/文件流
内置对 Symfony 的 BinaryFileResponse
和 StreamedJsonResponse
的完全支持。要使 StreamedResponse
完全可流式传输,您需要将 callback
改为 \Generator
,将所有 echo
替换为 yield
。查看示例
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"; } ); } }
会话
目前,Symfony 可能会与工作模式下的会话(例如,丢失登录用户或相反,由于缺少全局变量而导致登录用户会话泄露到另一个请求)苦苦挣扎。(解释见文末)。
捆绑引入了 FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory
,该工厂正确处理原生会话。如果您使用 symfony/flex
(待 flex 菜谱 PR),则此工厂将默认自动注册。您也可以手动注册它,例如在 framework.yaml
中。
framework: session: storage_factory_id: FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory
Sentry
内置对 Sentry 的支持。只需像平时一样安装和配置即可。
composer require sentry/sentry-symfony
Centrifugo(WebSocket)
要启用 Centrifugo,您需要添加 roadrunner-php/centrifugo
包。
composer require roadrunner-php/centrifugo
捆绑使用 Symfony 的事件调度器。您可以为任何扩展 FluffyDiscord\RoadRunnerBundle\Event\Centrifugo\CentrifugoEventInterface
的事件创建 事件监听器
ConnectEvent
必需 :)InvalidEvent
PublishEvent
RefreshEvent
RPCEvent
SubRefreshEvent
SubscribeEvent
示例用法
<?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 ], )); } }
请注意,如果您没有设置任何响应,则捆绑将默认发送 DisconnectResponse
。
使用 Symfony 和 RoadRunner 进行开发
- 如果可能,停止在您的服务中使用延迟加载,立即注入服务。
- 这不再需要,并可能给您带来潜在问题,例如内存泄漏。
- 不要在您的服务中使用/创建本地类/数组缓存。尝试使它们无状态;如果它们不能是无状态的,请在每个请求之前添加 ResetInterface 来清理。
- Symfony 表单可能会由于本地缓存而导致数据在请求之间泄漏。确保您的表单
defaultOptions
是无状态的。不要存储任何敏感/重要信息,否则它将在后续请求中泄露。 - 通过利用
EquatableInterface
和自定义反/序列化逻辑来简化User
会话序列化。这将防止由于断开的 Doctrine 实体而导致的错误,并且作为额外的奖励,将加快从会话中加载用户的速度。
<?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() ; } }
调试(建议)
使用RoadRunner时,您不能简单地“丢弃并死亡”,因为没有东西会被打印出来。我想介绍Buggregator来解决这个问题。作为额外的好处,它还可以作为mailtrap或本地测试Sentry
致谢
灵感来源于现有的解决方案,如Baldinof的Bundle和Nyholm的Runtime