把两个现成的应用塞进一个跑了八年的老站点——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/
<!doctype html><meta charset="utf-8"><title>老站点</title><h1>我是跑了八年的老站点</h1>
$ curl http://127.0.0.1:8099/app/index.php
HOST: <a href="/app/index.php?_r=blog%2Findex">blog</a> | <a href="/app/index.php?_r=shop%2Findex">shop</a>
$ 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 就是全部(下面这段我实跑通过):
<?php
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/blog/BlogApp.php';
require_once __DIR__ . '/shop/ShopApp.php';
use DuckPhp\DuckPhpAllInOne;
class SiteApp extends DuckPhpAllInOne
{
public $options = [
'path' => __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: <a href="' . __url('blog/index') . '">blog</a>'
. ' | <a href="' . __url('shop/index') . '">shop</a>';
}
}
SiteApp::RunQuickly([]);
子应用是自己声明自己的普通应用类,父应用不碰它的内部:
<?php
namespace Blog;
use DuckPhp\DuckPhpAllInOne;
class BlogApp extends DuckPhpAllInOne
{
public $options = [
'path' => __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 及其 Controller 骨架类);现成的管理后台 + 用户系统由同作者的 Composer 包 dvaknheo/duckadmin 提供:
composer require dvaknheo/duckadmin
它本身就是一个 DuckPHP 应用,可以按上一节的方式当成子应用挂进来;具体用法以 DuckAdmin 的文档为准。
注:DuckAdmin 要到稍后的版本才会稳定,这一节没有实测——先了解,别急着上生产。
顺手说几句它的底色
- 零依赖:
composer require dvaknheo/duckphp,require里只有php >= 7.4,依赖树里不会多出一堆传递依赖。 - 库模式:它是你项目的一个依赖,不是"你得搬进它的骨架"。骨架工程(
./vendor/bin/duckphp new)是可选的。 - 全组件可替换:
View、Cache、DbManager、Logger……都是可变单例,老项目里已经有一套封装时,换装配就行,业务代码不动。 - 不造抽象:不做模板引擎( PHP 就是模板)、不做 ORM ( PDO 薄封装,SQL 在日志里看得见)、不套中间件栈(堆栈清晰,出错好查)。
- 质量:95 个测试用例、892 个断言、
src/行覆盖率 100%( 4737/4737 );文档 47 章用户指南 + 112 篇参考手册,中英双语。
什么情况下别用它
- 你要的是开箱即用的后台管理 + CRUD 生成器——它给的是机制,业务自己写;
- 你需要庞大的中间件生态(队列、限流、验证器全家桶)——它不是这个路线;
试一下
composer require dvaknheo/duckphp
./vendor/bin/duckphp new # 可选:生成骨架工程
- Gitee:https://gitee.com/dvaknheo/duckphp
- GitHub:https://github.com/dvaknheo/duckphp
- 文档:仓库内
docs/zh/index.md(中文)、docs/en/(英文) - 版本:Packagist 上
dvaknheo/duckphp1.4.1