本站点已经停止维护,更多关于 Laravel 的内容请前往 laravel-china.org

面对现实吧!维护大型 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.phpapp/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.phpAcl.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 AqibLarge Scale Laravel Application
相关讨论请到 Laravel China 社区:https://laravel-china.org/articles/4630/wake-up-and-smell-the-coffee-maintenance-of-large-php-applications-is-not-simple

JokerLinly
作者 JokerLinly
Stay Hungry, Stay Foolish.