dvaknheo
V2EX  ›  PHP

DuckPhp 1.4.1 发布

  •  
  •   dvaknheo · 7h 59m ago · 584 views

    把两个现成的应用塞进一个跑了八年的老站点——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
    

    注意三件事:

    1. 老站点原样活着,/ 还是那个静态页;
    2. 新应用就挂在 /app/ 下,没有改任何 rewrite 配置;
    3. 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          # 可选:生成骨架工程
    
    2 replies  •  2026-10-19 22:37:31 +08:00
    dvaknheo
        1
    dvaknheo  
    OP
       7h 39m ago
    距离当初在 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 里面有例子)
    yafoo
        2
    yafoo  
       6h 3m ago via Android
    感觉以后做这种开发没什么意义了,我弄了个框架,投入了好多精力,感觉也没啥意义了
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Privacy   ·   Solana   ·   924 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 20:40 · PVG 04:40 · LAX 13:40 · JFK 16:40
    ♥ Do have faith in what you're doing.