FrankenPHP 运行 Symfony 项目需适配其架构:正确设置 public/ 文档根、worker 模式下避免容器状态错乱、显式传递环境变量与 php.ini、用 Caddy 语法重写路由,并注意 Kernel::terminate 不触发需改用事件监听器。

直接上 FrankenPHP 跑 Symfony 项目是可行的,但不能照搬 Nginx+PHP-FPM 的那一套配置和目录结构——否则会遇到 404、500、环境变量丢失、或 worker 模式下容器状态错乱等问题。
symfony serve 和 public/ 目录路径必须对齐
Symfony 默认用 symfony serve 启动时,它把 public/ 当作文档根(document root),所有请求都从这里进。FrankenPHP 同样依赖这个约定,但它不会自动识别你的项目结构。
- 如果你用 Docker 部署,
COPY . /app/public是错的——这会把整个项目(含src/、config/)塞进/app/public,导致路由失效 - 正确做法是只复制
public/下的内容到容器的根路径(如/app),再通过Caddyfile显式指定root * /app - 本地测试时,运行
frankenphp php-server --document-root=public,别漏掉--document-root参数 - 若用
composer create-project symfony/skeleton新建项目,确保public/index.php存在且可执行(FrankenPHP 不会帮你生成它)
worker 模式下不能直接复用 $container->get()
开启 worker 模式后,Symfony 容器只初始化一次,后续请求共享同一个 $container 实例。这听起来很香,但容易踩坑:
-
$container->get('request_stack')返回的是上一个请求残留的Request对象,不是当前请求的 - 自定义服务如果在构造函数里读取了
$_SERVER或$_ENV,这些值可能来自第一次启动时的环境,而非当前请求上下文 - 解决方法:改用
$container->get('request_stack')->getCurrentRequest();或把依赖注入改为Request对象本身(由容器按需注入) - 不要在
__construct()中做任何与请求强相关的初始化,移到__invoke()或控制器方法里
环境变量和 php.ini 配置需显式传递
FrankenPHP 不继承系统级 php.ini,也不自动加载 .env 文件——它只认你给它的那一份配置。
立即学习“PHP免费学习笔记(深入)”;
- Docker 镜像默认用
php.ini-production,但 Symfony 常依赖opcache.enable=1、date.timezone等设置,建议挂载自定义php.ini到/usr/local/etc/php/conf.d/99-custom.ini -
APP_ENV=prod、APP_DEBUG=0这类变量必须通过ENV指令写进Dockerfile,或在Caddyfile里用php env APP_ENV prod设置 -
.env文件不会被自动读取;要么提前用composer dump-env prod编译成.env.local.php,要么在index.php开头手动Dotenv::createUnsafeImmutable(__DIR__.'/..')->load(); - 注意:worker 模式下,
$_ENV在首次启动后就固化了,运行时改环境变量无效
Caddyfile 中的重写规则要适配 Symfony 路由
FrankenPHP 自带 Caddy,所以你得用 Caddy 语法写重写,而不是 Nginx 的 try_files。
- 错误写法:
try_files $uri $uri/ /index.php?$query_string(这是 Nginx 语法,Caddy 不认) - 正确写法:在
Caddyfile的handle块里加php指令,并配合rewrite:
handle *.php {
php
}
handle {
rewrite * /index.php?{http.request.uri.path}&{http.request.uri.query}
}php 指令 + split_path,它会自动处理 index.php 前缀和 PATH_INFOCaddyfile 中显式允许 HTTP/2 和 WebSocket 协议,否则 SSE 或订阅会断连最常被忽略的一点是:worker 模式下,Kernel::boot() 只执行一次,但 Kernel::terminate() 不会被调用——这意味着你不能靠它来清理资源或刷缓存。所有“每次请求结束”的逻辑,得改用事件监听器(如 kernel.finish_request)或中间件来兜底。



















