面对现实吧!维护大型 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 命令来自动执行文件生成和分离过程。完成后,我会添加包链接分享出来。
在此之前,如果有更好的意见请分享,我需要你的帮助才能找到最佳解决方案。
周五了,周末愉快:beers:
本文翻译改编自 Tawsif Aqib 的 Large Scale Laravel Application
本作品采用《CC 协议》,转载必须注明作者和本文链接
高认可度评论:
很好的话题。
但是,有时候我会觉得这是:为了让事情不复杂,找了一个更加复杂的方案呢。
最近在思考如何组织代码结构,觉得按类别划分,搞了一堆 Services、Repositories、Transformers、Managers,还是把一堆业务塞在了同一个目录中。
而按业务划分:Warehouse(仓库)、Product(商品)、Activity(活动)、Order(订单)、Risk(风控)、 Logistics(物流)、UserCenter(用户中心),当系统变大后,甚至可以直接单独拆分为独立的系统(或子系统),这样也比较符合现在微服务的概念。
很好的话题。
但是,有时候我会觉得这是:为了让事情不复杂,找了一个更加复杂的方案呢。
这就好比想用设计模式让代码更容易维护,实际上确无形增加维护成本,这就是为了设计而去设计的结果。
这些想法的初衷都觉得未来可能是个大项目,要设计得多么优雅多么可维护性,目录多么清晰整洁,实际上没作用性不大,给认为这样很清晰的洁癖的人看而已。
太过于考虑后期维护而在前期设计一堆认为后期有用的东西,当真正到了维护期基本不会觉得当初的设计合理有用。
有实际的案例配合就更好了,原文看起来更像一些思考和尝试。
在大型项目中确实会面临这样的问题,我们就是从把一个大的app拆分成多个小的app,基本思路和这个比较类似。
最近在思考如何组织代码结构,觉得按类别划分,搞了一堆 Services、Repositories、Transformers、Managers,还是把一堆业务塞在了同一个目录中。
而按业务划分:Warehouse(仓库)、Product(商品)、Activity(活动)、Order(订单)、Risk(风控)、 Logistics(物流)、UserCenter(用户中心),当系统变大后,甚至可以直接单独拆分为独立的系统(或子系统),这样也比较符合现在微服务的概念。
@MrJing 由
Controller
调用各个业务模块提供的接口吗?还是说Controller
也放到各个模块中?其实 项目会变成什么样很难预测啊。
请教一个问题,Repositories这个是模型层与业务层的中间层么?
我看laravel的教程上面基本都没有介绍到它。
@Summer
我同事就是这样评价laravel的
@MrJing 表现层通过api-gateway向业务层请求资源, 业务层需要向数据层请求数据, 那像你这样划分的话, 比如我要查看某张订单里面的商品信息, 这些商品信息是哪里来的.
从业务层的订单向数据层的商品获得数据, 但是
文章 这篇文章就提到每个服务都有自己的数据, 意思是订单数据层中的商品数据是冗余吗.
还是说是在表现层获得当前订单里面的商品id, 然后再从表现层通过api-gateway去跟业务层的商品模块获取商品数据(要是这样,一来一回不会费时间). :joy:
公司项目已经遇到了这种情况。:scream:
@jobsssss 我也感觉laravel真的很复杂。俗话说不想偷懒的程序员不是好程序员。服务容器啊,一系列的手段,都是想要代码复用,并能更好的维护。但是构建这些东西本来就是很复杂的一件事,你去看看laravel很多源码很烧脑的,就是因为要达到尽量的复用代码,可能你会发现这段代码是父类调用子类,有的代码是子类调用父类,单独来看还好,但是掺杂在一起了,就真的是烧脑了。特别是遇到问题调试的时候,脑回路得很大。这可能就是为了“简单”,而要先复杂的代价吧。
我是把每个部分当扩展包来做的。
@william 同一个系统中,我觉得 controller 集中在起一起没有什么问题,本来 controller 就是框架中的概念。而且,Controllers 目录中也是可以分目录划分的。
我们讨论的是对于复杂的业务本身如何去切分,我觉得这也是 Laravel 中移除了 Models 目录的主要原因,因为使用者都喜欢将所有业务逻辑都放在 Models 中,而不去按照业务本身去分层或者切分。
@郑方方 像京东、淘宝这种大型电商系统中,各个业务应该都是由不同业务部门和团队去开发和维护的。可能商品类目类型和订单系统压根就分离开的。那么一个订单中有哪几个商品,这个怎么获取?首先,我觉得各个系统去冗余数据,肯定不是什么好方案,维护数据一致性太麻烦了,而且有点差错就有问题了;而且让做订单的也去维护商品逻辑,不好,各搞一套,容易出错。一定是商品系统对外提高服务或者接口,让订单系统去调用。一来一去是费时间,但是 RPC(远程调用)不仅仅只有基于 http 协议的,还有基于 tcp、基于 socket,应该算比较快速的。
@郑方方 我说得也可能不对,为了性能什么的,可能还是会缓存数据,缓存也算冗余数据的一种吧。毕竟每次都调接口去查,量太多肯定也是不合适的。能肯定的是:各个子系统之间有依赖的话,一定会提供接口的。
@MrJing :pray: 各个服务之间有专门暴露给其他服务的接口, 也有专门暴露给表现层的接口.
恩恩,这样我就明白多了,谢了:+1:
@Insua Laravel确实有点封装过度
@jobsssss
我一般是会按照PSR-4来划分业务为多个app,喜欢Laravel的主要原因就是很自由,所以Github上很多Laravel项目都各有特色地安排项目目录,不过话说回来,一个web项目纯基于业务逻辑庞大也不会大到什么程度,随着业务,数据了,并发等等实际情况变的庞大的情况出现以后就会逐渐划分成多个微服务了,架构初期合理清晰地设定项目目录结构就好,按照业务设计就好。
@MrJing 按项目大小来应用这两种形式感觉是很不错的,稍微简单的项目按类别划分,复杂的项目按业务分,也可以说是模块化 。???
从头到尾都是一个人的项目,一个人维护
php的本质就是为了快速开发。没必要整的那么复杂,弄一些高大上的词语来装饰自己。
微服务就好,也便于横向扩展。
确实,已经遇到了这种情况,现在正在尝试项目拆分。
有点复杂,不过总觉得还是放一起比较好。假设不同的微服务放在不同的服务器上,rest接口的关联操作就很费劲了。一个订单查出来会关联用户 产品 支付 物流等等,甚至还有更深层级关联如:用户的相关订单,产品的推荐产品,那最终效果就是一个页面的接口要跨好几个服务器查询,但是分别做适度冗余岂不是又接近于整合到一起的思路了?
@genyii 不是说分开就什么玩意都拿出来分,并不是这个意思。比如说用户登录相关的操作弄成一个微服务,这个微服务就只做用户登录,其他订单、积分之类的东西就不要放进去了。是这个意思。
记得有一个包nwidart/laravel-modules :star:
我是这样拆的:
大目录:controllers, services(抽象部分controllers逻辑,最后controllers只剩控制流),validations,models,repositories. 其中models和repositories放在各自模块建立的包里。
小目录:外层小目录根据模块拆分
我比较纳闷的是,都把消息队列给放哪里去了,解决掉各个services之间的耦合性,通过其他的方式来传递消息,那么如果我想要拆封开一个服务,不就是把controller搬一次就好了么...
按照功能模块分是很好的解决方案,后期项目需要拆分就十分方便。
这句话可能有错别字。
對於 Voten 開源, 大家有什麼看法呢?
好像沒有用任何的 architecture 呢
@Openset 阔以阔以,改过来了
@JokerLinly 领域驱动设计(DDD),有一个地方少了一个"领"。不要打我,我不是来找毛病的,就是不小心看到了。
@Openset ?积极认真的娃
项目超过100个模块,已经被迫拆分成多个系统(3个Laravel,几个Lumen)。
(MD,谁TMD能想到当年一个7天的小破私活,干到这个规模。。。。。)
迭代速度现在越来越慢了~
暂时也想不到什么好办法解决困境~
而且现在功能还在不断的累加中~
哎~
不到 微服务那个级别的话,最好是采用模块化开发;将一个 模块所使用的模型,控制器,视图、单元测试以及路由单独发布;
使用composer统一依赖到一个项目当中;symfony框架对这种方式有着得天独厚的优势,参考sylius
能有一个好的解决方案并去尝试是一件好事,首先已经抛出这个问题了,总有他的用处,好的东西都是这样讨论出来的。收藏了万一以后有用了,来翻下
模块化的设计,Yii2做得不错。
的确,大型项目的确要好好的规划一下目录结构。
我们的项目目录是这样的:
每个module下的结构跟框架的根目录结构差不多,每个module都可以灵活的控制。基于https://github.com/caffeinated/modules实现。
@sunrise 分多个module,最终还是要走上每个module变成一个独立的 application 模块分布式
公司项目是这样的目录结构..共用
core
虽然很想说设计的有些复杂,其实是为了简化代码,感觉有些互相矛盾,
这个目录结构的设计时候考虑一下性能,如果是多端的话,service就做数据提供者就好。
本文适用于业务比较复杂的场景。
随着业务复杂度提升,单纯的靠文件夹约定区分不同业务已经有点力不能及,技术架构需要调整,单独定义一个服务层(或者service,代表一个意思)。
再回到本文讲解的hmvc结构,本质就是服务层的一种目录结构方式。
hmvc这种目录结构方式,定义(或者保留)了业务和服务层的目录结构,最大优点是业务、服务层不用做很大的调整,不足之处是没有定义业务和服务层的通信方式,把这个锅扔给了架构师(或者技术经理)。
有 demo 吗?
Transformers 文件夹下面一般放什么文件。 HMVC 的 'H' 代表的是什么?
看了文章之后,再回过头看项目,多了很多思考,包括目录结构上的设计