DuckPhp 1.4.1 发布

dvaknheo 2026-10-19 20:41 1

把两个现成的应用塞进一个跑了八年的老站点——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 # 可选:生成骨架工程


  • Gitee:https://gitee.com/dvaknheo/duckphp

  • GitHub:https://github.com/dvaknheo/duckphp

  • 文档:仓库内 docs/zh/index.md(中文)、docs/en/(英文)

  • 版本:Packagist 上 dvaknheo/duckphp 1.4.1

最新回复 (4)
  • dvaknheo 楼主 10-19 21:01
    1楼
    距离当初在 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 10-19 22:37
    2楼
    感觉以后做这种开发没什么意义了,我弄了个框架,投入了好多精力,感觉也没啥意义了
  • king888 10-20 09:48
    3楼
    感觉在 ai 面前像个小丑,确实没有意义了
  • iorilu 10-20 10:04
    4楼
    都 ai 了

    还折腾啥

    让 ai 把 php 换成流行的架构把
    建议换 go
* 帖子来源V2EX
返回