• dvaknheo

    把两个现成的应用塞进一个跑了八年的老站点——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&quot;
    [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/duckphp 1.4.1

    主楼
  • dvaknheo

    距离当初在 V2EX 上发布 duckphp 已经不知道过了多少年了。
    这两年陆陆续续开发,24 年年底的时候可能要发布一个稳定版本。但后面没动力发了,反正发了也没人会看
    到去年年中又写了一阵。
    今年 5 月的时候,心血来潮,把 1.3 系列发布并算了结了。

    1.4 系列架构调整,终于 完美解决框架互嵌 的问题
    后面各种小修复,还花了很长时间折腾 管理员和用户接口,尽管如此,管理员和用户接口还是我觉得不够完美的。
    其他普通的应用不会涉及到那么深, 毕竟 DuckPhp 的理念就是 普通开发 和核心开发分开。 普通开发的功能基本都没怎么改,就是高级功能调整很多

    在开发 DuckPhp 过程中,也不只是开发 dvaknheo/duckphp 这个包, 最重要的还是 dvaknheo/duckadmin 这个管理后台系统, 这个管理后台系统 最大的特点就是 你的代码不必在管理后台上二次开发,而是直接作为包引入。
    在 1.4.1 发布的这个时候,duckadmin 还是在微调中,稍后我再发布
    dvaknheo/duckcoverage 则是支持包, 支持你的 duckphp 工程的覆盖测试。
    更底层的是 之前稳定发布的 dvaknheo/libcoverage 这个包的作用是让你写的 composer 包代码全部覆盖测试
    这几个包都有发布版本
    workermanhttpd swoolehttpd 则是我在 1.4 之后,用 ai 改进了一下,个人没有测试,也只是推送而不发布版本
    作用是 workerman /swoole 底下的运行时

    这些天还新写了 duckmcp 。作为 mcp 框架
    最后一个没动的是 n 年前写的 tagfeather 有时间我把它完成

    在 deepseek 的帮助下,代码写得比以前效率高多了
    还有,文档都是 deepseek 写的

    写代码心里有数,写文档不知道读者看明白了没,写推广两眼一抹黑
    哈哈,上面的推广其实也是 deepseek 帮忙写的。
    duckphp 是有 agent.md 可以让 ai 来写, 也可以用 laravel 或 thinkphp 的 orm (duckadmin 里面有例子)

    #1
  • yafoo

    感觉以后做这种开发没什么意义了,我弄了个框架,投入了好多精力,感觉也没啥意义了

    #2
  • king888

    感觉在 ai 面前像个小丑,确实没有意义了

    #3
  • iorilu

    都 ai 了

    还折腾啥

    让 ai 把 php 换成流行的架构把
    建议换 go

    #4

发表回复

登录后回复