“文件夹只是建议,没有阻止导入。”大概每个读完六边形架构文章的 PHP 开发者,都会经历这样一次幻灭:你对着那张漂亮的六边形图画出 Domain、Application、Infrastructure 三个文件夹,心里已经把自己归类到架构整洁派了。可两周之后,一个 use Doctrine\ORM\EntityManagerInterface 赫然坐在某个领域实体的文件顶部,并且没人记得是谁批的。你以为有了三层文件夹就防住了腐化,但编写代码时根本没有东西替你挡住那只伸向内层的手。

这篇文章就给你一套硬梆梆的规矩——不是观念,是能直接落到 composer.json、目录树和 CI 流水线里的东西。目标运行环境是 PHP 8.4+,搭配标准的 PSR-4 自动加载,但思路对所有版本都成立。

打开网易新闻 查看精彩图片

先看目录。整个 src/ 底下只有三个顶级命名空间,和磁盘上的三个目录严丝合缝地对应:

src/Domain/ ——这是纯 PHP 的领地,连一行 vendor 里的 import 都不该出现。里面按业务拆分子目录,比如 Order/Order.phpOrder/OrderId.phpOrder/OrderStatus.php,还有 Customer/ 和像 Shared/Money.php 这样的横切值对象。

src/Application/ ——用例和端口呆的地方。 Order/PlaceOrder.phpPlaceOrderInput.php 表达了“下订单”这件事,而 Port/OrderRepository.phpPort/PaymentGateway.phpPort/Clock.php 则是用例们对外提要求的接口。

src/Infrastructure/ ——这才是真正与外界打交道的一层。框架的控制器、Doctrine 的持久化实现、Stripe 支付适配器、系统时钟的封装,以及装配一切的引导容器,全部塞在这里。 Persistence/Doctrine/Http/Controller/Payment/Stripe/Clock/SystemClock.phpBootstrap/Container.php 各自归位。

这三层就是一个同心环,Domain 是圆心,Infrastructure 是接触真实世界的外缘,Application 夹在中间,装着用例和用例所依赖的接口(端口)。

目录定下来之后,下一步是让命名空间把架构讲清楚。 composer.json 里只保留一行 PSR-4 声明:"App\\": "src/" 。这意味着 src/Domain/Order/Order.php 的完整类名就是 App\Domain\Order\OrderApp\ 之后的第一段立刻告诉你它属于哪个层,连脑内解码的时间都省了。那种把各种概念拍平在根命名空间下的做法——比如 App\ModelsApp\ServicesApp\Http——恰恰抹掉了层的承重作用。在六边形架构里,层是第一优先级的信息,所以它必须出现在最前面。同理,也请别把框架自带的 App\ControllerApp\Http 约定搬进来,它们习惯把所有东西揉成一个袋子,而你要做的恰恰相反。

有了目录和命名空间,还得有导入方向这个“门禁”。规则就是一句话:导入永远指向内层,绝不向外。想象一下套娃,外头看得见里头,里头不知道外头的存在。

具体到这三个层,Domain 是最内圈,只能导入自身的类。任何朝向 vendor、甚至朝向 Application 的 import 都算犯规。Application 是中间层,可以导入 Domain 和自己,但正常情况下决不允许拉进 vendor 里的东西——只有一个公开的特例:PSR 接口(比如 Psr\Log\LoggerInterfacePsr\Clock\ClockInterface)以及纯库的价值类型,是被放行的。原文只点到这几个名字,你按同样的思路去判断其他候选即可。最外头的 Infrastructrue 没有限制,它本就该导入所有层,因为它的全部工作就是在外部世界和内部腔体之间当翻译。

读到这里你可能已经感觉到了,这套规则单靠人工 code review 是守不住的,一定会被时间磨没。所以文章还给出了一条最后的防线:在 CI 阶段加一个检查,一旦有人让 import 指错了方向,构建就直接挂掉。具体用什么工具实现不是这里的重点,但你只要知道,边界执行的严肃程度已经可以提到“不通过就进不了仓库”这个档次了。

把目录建好只是给你在地面上画了几条线,导入规则和自动化检查才是真正砌起来的墙。下次当那个 use Doctrine 再次出现在 Domain 目录的时候,你至少能够理直气壮地说:不是“建议”,而是“禁止”。