面对现实吧!维护大型 PHP 应用程序不简单!
(如果你很着急,可以直接跳到 解决办法 ~)
我们都知道 Laravel 是迄今为止最受欢迎的 PHP 框架。 它的目录结构好、组织有序、定义简单。当我们在一个中小型项目工作时,使用 Laravel 提供目录结构是非常友好的。 但是,当它开始成为一个超过 50 个模型的大型应用程序时,我们就已经是一脚踩进自己埋的坑里面了。且再难回头!
维护一个大的应用程序真的不是开玩笑的,它是需要好好地被思考和设计。而 Laravel 的默认的目录结构对于这种情况明显是心有余而力不足的。
首先,我们可以来看看 Laravel 默认的目录成为大型应用程序产生的变化。
Laravel 默认的目录结构就像这样:
|- app/
|- Console/
|- Commands/
|- Events/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Jobs/
|- Listeners/
|- Providers/
|- User.php
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
这样的目录结构设计没有任何问题。 而当我们的业务逻辑稍微复杂一点时,我们通常会用 Repositories、Transformers 等这些个文件夹来划分。 跟下面这个差不多:
|- app/
|- Console/
|- Commands/
|- Events/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Jobs/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
这显然是一个目录结构设计友好的 Laravel 项目。 看看 Models 文件夹里面:
|- app/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Merchant.php
|- Store.php
|- Product.php
|- Category.php
|- Tag.php
|- Client.php
|- Delivery.php
|- Invoice.php
|- Wallet.php
|- Payment.php
|- Report.php
看上去也不是那么糟糕对吧!这里面也建立了一个文件夹 Services 专门处理所有的业务逻辑。还有 Repositories、Transformers、Validators 这些有着差不多相同的类的文件夹。很多人也觉得这样的设计很不错,并且乐于这样去设计。不过完成仅仅只是单个实体 / 模型的工作需要浏览不同的文件夹和文件,即操作很多类,写各种接口,光是这一点让一些开发者觉得很麻烦。
但问题的关键不在于用不同的文件夹去划分,而是开发者维护代码和服务之间的通信。
分析下前面的代码结构可以看到:
- 这是一个 庞大 的应用程序
- 对于一些开发人员来说,很难 维护
- 生产力 低(在考虑系统内部连接上需要耗费时间)
- 代码的 规模调整 也是一个问题
解决方案是显而易见的 —— 微服务。即使我们使用 SOA(面向服务架构),我们还是需要将我们的庞大的应用分解成较小的独立的部分,以便日后将其分开扩展。照理来说这个解决方法挺好的。但现实中我们并没有这么做。因为对于将代码分解成更小的部分这件事情,说的通常比做的要容易得多得多~
通常分离服务需要两个简单的步骤:
- 将子服务(Models、Repositories、Transformers 等)移动到新的 PHP 微服务应用程序中
- 重新确认服务函数调用的目标确确实实指向到新的微服务中(例如,创建 HTTP 请求)
然后你需要查找与该服务相关的所有文件。你可能会很惊奇地发现,我们可能有部分代码不经过服务而直接使用了它的模型或存储库(我真的干过这种事)。而这还不是唯一的问题,我们甚至可以总结一下:
- 有太多的文件要考虑
- 犯错误的机会很高
- 容易让开发者沮丧
- 有时需要重新考虑域名之间的逻辑
- 新的开发者?还是洗洗睡吧!
最后一个原因非常重要,因为新的开发人员很难在短时间内掌握整个应用程序。而通常项目经理不会给他太多的时间去研究。这容易产生给内置对象扩展方法、代码放在错误的位置、甚至会让下一个新的开发人员更加困惑等问题。
幸运的是,我们已经有一个解决方案 —— HMVC。即将整个应用程序分成较小的部分,每个部分都有自己的文件和文件夹,如 app/
文件夹,并通过 composer.json 自动加载,如下所示:
|- auth/
|- Exceptions/
|- Http/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- merchant/
|- Console/
|- Events/
|- Exceptions/
|- Http/
|- Jobs/
|- Listeners/
|- Models/
|- Presenters/
|- Providers/
|- Repositories/
|- Services/
|- Transformers/
|- Validators/
|- database/
|- factories/
|- migrations/
|- seeders
|- config/
|- routes/
|- resources/
|- assets/
|- lang/
|- views/
但当我们想要将特定模块移动到微服务中时,HMVC 让事情变得复杂起来,因为我们还是需要在主要代码库中保留控制器、中间件等。大多数时候,将代码移动到微服务会需要重新定义路由和控制器。这里面有太多不必要的工作,除开我很懒这个理由,正常的开发者都只想分开那些不得不分开的东西。因此我不是很推崇这种目录结构。
领域驱动设计也可以是一个解决方案#
没有完美的解决方案,凡事都有两面性,还是要看每个人偏好。我们不会在这里讨论领域驱动设计(DDD),但总的来说,DDD (可能)将你的 Laravel 应用程序构建为 4 部分:
- 应用程序(Application) —— 掌管 控制器、中间件、路由
- 领域(Domain)—— 掌管业务逻辑 Model、Repository、Transform、Policy 等
- 基础设施(Infrastructure) —— 掌管 Logging、Email 等常见服务
- 接口(Interface) —— 掌管 View 、lang、assets
这看起来很容易,那么为什么我们不这样构建我们的应用程序,并使用命名空间?
|- app/
|- Http/ (Application)
|- Controllers/
|- Middleware/
|- Domain/
|- Models/
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Services/
|- Infrastructure/
|- Console/
|- Exceptions/
|- Providers/
|- Events/
|- Jobs/
|- Listeners/
|- resources/ (Interface)
|- assets/
|- lang/
|- views/
|- routes/
|- api.php
|- web.php
因为将项目分割成文件夹是不行的。 这仅仅只是意味着我们只添加了一个父命名空间。
真理的时刻#
你能看到这里也是挺了不起的了,毕竟好像哪个方法都行不通。我们还是来好好聊一聊真的解决方案吧!看看下面的目录结构:
|- app/
|- Http/
|- Controllers/
|- Middleware/
|- Providers/
|- Account/
|- Console/
|- Exceptions/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
|- Merchant/
|- Payment/
|- Invoice/
|- resources/
|- routes/
Auth.php
and Acl.php
是 app/Account/
文件夹中的服务文件。 控制器只能访问这两个类并调用它们的方法。 其他类永远不会知道 app/Account/
文件夹中其他剩下的类。 这些服务中的方法将仅接收基本的 PHP 数据类型,例如 array、string、int、bool 和 POPO(Plain Old PHP Object),但没有类实例。 示例 :
...
public function register(array $attr) {
...
}
public function login(array $credentials) {
...
}
public function logout() {
...
}
...
这里要注意一个地方,register 函数接收一个数组属性的参数,而不是 User 对象。 这个点很重要,因为让其他类去调用该函数的时,不应该让这个函数知道 User 模型的存在。这是这整个结构你必须要遵守的基本规则。
当我们想分开代码#
我们的应用程序变得越来越大时,我们希望将 Account
相关的内容分开到单独的微服务中并将其转换为 OAuth
服务器。
所以,我们只是需要移动以下部分
|- Account/
|- Console/
|- Exceptions/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
如果是将应用程序搬到 Lumen:
|- app/
|- Http/
|- Controllers/
| - Middleware/
|- Account/
|- Events/
|- Jobs/
|- Listeners/
|- Models/
|- User.php
|- Role.php
|- Permission.php
|- Repositories/
|- Presenters/
|- Transformers/
|- Validators/
|- Auth.php
|- Acl.php
|- routes/
|- resources/
不可避免地我们必须在控制器和路由中编写代码,因为我们需要使其成为一个 OAuth
服务器。
那么接下来我们需要在主要代码库中做什么改变?
我们只需要保留服务文件 Auth.php
和 Acl.php
,并将其方法中的代码更改为针对新创建的微服务的 HTTP 请求(或其他信息传递方式)。
...
public function login(array $credentials) {
// change the code here
}
...
整个应用程序将保持不变。 而应用程序的目录结构将如下所示:
|- app/
|- Console/
|- Exceptions/
|- Http/
|- Controllers/
|- Middleware/
|- Providers/
|- Account/
|- Auth.php
|- Acl.php
|- Merchant/
|- Payment/
|- Invoice/
|- resources/
|- routes/
这个方法只做了很少的事情,就将部分代码移动到完全独立的微服务中。(反正我暂时是想不到更好的方法了)
权衡#
正如上面所说的,任何事情都有一个权衡点,对于这个解决方案来说也一样。而且,这里我们有个关于 迁移 的问题!因为在上述文件夹结构(分离之前)所有的迁移文件都是在 database/migrations/
目录中。但是,当我们要分离一个域时,我们需要确定并移动该域的迁移。这件事情有点难办,因为我们没有明确指出哪个迁移属于哪个域。我们可能需要研究如何将标识符放在迁移文件中。
该标识符可以是域前缀。例如,我们可以命名迁移文件 xxxxxxxxx_create_account_users_table.php
而不是 xxxxxxxxx_create_users_table.php
。如果我们想要,我们也可以使用 account_users
表名替换 user
。我更喜欢在分离过程中识别哪些表要移动。分离迁移文件可能有点令人沮丧,但是如果我们使用前缀或任何类型的标记,那么这整个过程肯定会变得不那么痛苦。
我还在计划尝试构建一个 Laravel 软件包的结构,提供 artisan 命令来自动执行文件生成和分离过程。完成后,我会添加包链接分享出来。
在此之前,如果有更好的意见请分享,我需要你的帮助才能找到最佳解决方案。
周五了,周末愉快
本文翻译改编自 Tawsif Aqib 的 Large Scale Laravel Application
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: