- SignalDesk2小时前
把两个现成的应用塞进一个跑了八年的老站点——DuckPHP 的多应用互嵌 本文所有输出都是在本机( PHP 8.2 + DuckPHP 1.4.1 )实跑得到的结果,代码可以照着抄。 先说场景 你手上有一个跑了八年的老站点:一堆 PHP 脚本、静态页、可能还有别人写的小工具。现在要加三样东西: 一个后台管理界面; 一个内部博客; 一个给 App 用的 API 。 常见的路数是"上框架"——然后你发现:要把整个站点搬进它的目录结构,要改 nginx 的 rewrite ,要把老代码改造成它的控制器,还得祈祷它和站点里那套老 Session/老 DB 封装不打架。八年的东西,动一下就有人半夜打电话给你。 DuckPHP 走的是另一条路: 它不要求你搬家,它自己搬进来。 先看结果 我在本地搭了个"老站点", document_root 下只有一个静态页和新建的 app/ 目录: legacy-site/ ← 老站点,document_root 指向这里 ├── index.html ← 跑了八年的老页面(纯静态) └── app/ ← 新加的 DuckPHP 应用 ├── index.php ← 唯一入口 ├── blog/BlogApp.php └── shop/ShopApp.php 启动服务( php -S 127.0.0.1:8099 -t legacy-site ),三个请求: $ curl http://127.0.0.1:8099/ 老站点 我是跑了八年的老站点 $ curl http://127.0.0.1:8099/app/index.php HOST: blog | shop $ curl "http://127.0.0.1:8099/app/index.php?_r=blog%2Findex" [blog] path=/tmp/legacy/app/blog/ [blog] url=/app/index.php 注意三件事: 老站点原样活着 , / 还是那个静态页; 新应用就挂在 /app/ 下, 没有改任何 rewrite 配置 ; blog 和 shop 是两个 独立的 DuckPHP 应用 ,各自有各自的 path ( /tmp/legacy/app/blog/ 和 /tmp/legacy/app/shop/ ),只是被同一个入口挂到了 /app/index.php?_r=blog/... 和 ?_r=shop/... 上。 上面输出里 path= 后面的绝对路径随你本机的目录而定。 它是怎么做到的 一个入口,一个 app 数组 app/index.php 就是全部(下面这段我实跑通过): __DIR__, 'controller_welcome_class' => SiteApp::class, 'app' => [ // ← 登记子应用 \Blog\BlogApp::class => [ 'name' => 'blog', 'controller_url_prefix' => 'blog/', // URL 空间:/blog/ ], \Shop\ShopApp::class => [ 'name' => 'shop', 'controller_url_prefix' => 'shop/', // URL 空间:/shop/ ], ], ]; public function action_index() { echo 'HOST: blog ' . ' | shop '; } } SiteApp::RunQuickly([]); 子应用是 自己声明自己 的普通应用类,父应用不碰它的内部: __DIR__, // 它自己的根:视图/配置/控制器都从这里找 'namespace' => 'Blog', // 它自己的命名空间 'controller_welcome_class' => BlogApp::class, ]; public function action_index() { echo '[blog] path=' . static::_()->options['path'] . "\n"; echo '[blog] url=' . __url('') . "\n"; } } shop/ShopApp.php 同理,只是 path 指向 shop/ 、前缀是 shop/ 。 两个关键点 path 管"文件从哪找", controller_url_prefix 管"URL 从哪开始匹配"。 前者决定子应用的视图、配置、控制器目录(所以两个子应用可以各带各的目录树,互不干扰);后者决定谁接管哪段 URL 。两者是分开的,所以你既能"子目录部署"( path 不变、前缀加目录名),也能"平铺挂载"(前缀就是应用名)。 不需要 PATH_INFO 。 无 PATH_INFO 路由是 默认开启 的: RouteHookPathInfoCompat 的 path_info_compact_enable 默认为 true ,所以 URL 里带的是 ?_r= (想要常规 PATH_INFO 就把它设成 false )。这带来一个很实际的好处: 老站点的 nginx/Apache 配置一行都不用改 ——你只要把新应用放到 document_root 下的某个目录、入口文件能被访问到,剩下的都归框架管。"不改配置"是它能在老项目里落地的关键。 底下是"相位( Phase )" 父子应用不是简单的 require 拼在一起:框架给每个应用一个 相位 ,装配、单例、异常处理都在各自相位里完成。所以父应用换了 View 实现,子应用不受影响;子应用自己的 path 、 namespace 、CLI 命令前缀也都是独立的。上面输出里两个子应用各自打出自己的 path ,就是这件事最直接的证据。 这件事为什么值得单独说 大多数框架的"模块化"是 代码层面 的:一个目录、一个命名空间、一个服务提供者。DuckPHP 的互嵌是 应用层面 的: 别人的应用 可以直接挂进来。同事写的、从 vendor/ 装的 DuckPHP 应用,只要类能被加载( composer 包连 autoload 都不用管),登记到 app 数组里就是一个子应用。框架自己的 E2E 测试( tests/data_for_tests/ZThirdDemo + tests/ZThirdDemoTest.php )就是拿"主应用 + third/ 里的第三方应用"这么跑的。 老项目可以渐进式改造 。先在 /app/ 挂一个小应用跑通,再一个模块一个模块往里搬;老代码一行不动,回滚就是删一个目录。 一个域名下可以有多个"独立应用"。博客、后台、API 各自一套目录和配置,各自能跑自己的 CLI 命令(带相位前缀),需要时还能用独立数据库连接( local_database ),但对外只是一个站点。 测试友好 。子应用能单独起、单独测,也能在父应用里做集成测试。 想要现成的后台和用户系统?可以接 DuckAdmin DuckPHP 本体只提供机制( GlobalUser / GlobalAdmin 及其 Controll
- 情报分类:技术学习与提效
- 分类依据:内容涉及技术、AI、软件工具或工程实践
- 信息来源:服务器 / V2EX
- 发布时间:2026/10/19 20:41:23
- 暂无回复