控制器篇
引言
控制器在任何基于 MVC(模型-视图-控制器)的项目中都扮演着巨大的角色。它们有效地扮演着"胶水"的角色,接收用户的请求,执行某种逻辑,然后返回响应。如果你曾经在任何相当大的项目中工作过,你会注意到你会有很多控制器,它们很快就会变得杂乱无章,你甚至没有意识到。在本篇教程中,我们将探讨如何在 Laravel 中清理臃肿的控制器。
目标读者
本篇教程适合初次接触 Laravel 开发并理解控制器基础的人。所讨论的概念相对简单,方便快速入门。
控制器臃肿的问题
臃肿的控制器会给开发者带来一些问题。它们会:
-
**让追踪特定代码或功能变得困难。**如果你想在一个臃肿的控制器中修改某段代码,你可能需要花很长时间追踪这个方法实际在哪一个控制器中。使用逻辑清晰的清洁控制器会更容易。
-
**难以发现错误的确切位置。**正如我们稍后在代码示例中将看到的,如果我们在一个地方处理授权、验证、业务逻辑和响应构建,那么找到错误的精确位置可能会变得困难。
-
**使编写复杂请求的测试变得更加困难。**复杂的控制器方法可能有很多行代码,并且做了很多不同的工作,有时这会导致测试变得困难。清理代码可以让测试变得更容易。我们将在第六篇详细讨论测试。
肿大的控制器
在本篇教程中,我们将以一个名为 UserController
的示例控制器为例:
为了使阅读更为简单,我没有在控制器中包含 index()
、create()
、edit()
、update()
和 delete()
方法。不过,我们会默认这些方法存在,并且我们也正在使用下面的技巧来清理这些方法。在本篇的大部分内容中,我们将专注于优化 store()
方法。
将验证和授权放入表单请求中
我们能够在控制器中做的第一件事就是将所有的验证和授权提取到表单请求类中。所以,让我们看看我们如何能够对控制器的 store()
方法执行这样的操作。
我们将使用以下 Artisan 命令创建一个新的表单请求:
php artisan make:request StoreUserRequest
以上命令会创建一个如以下所示的新的 app/Http/Requests/StoreUserRequest.php
类:
我们可以使用 authorize()
方法来确定用户是否应被允许执行此请求。如果他们能,则该方法应返回 true
,如果不能,则返回 false
。我们还可以使用 rules()
方法来指定应在请求体上执行的任何验证规则。这两种方法都将在我们需要手动调用任何一个之前自动运行。
因此,让我们从控制器的 store()
方法顶部移动我们的授权代码到 authorize()
方法中。完成此操作后,我们可以将验证规则从控制器中移动到 rules()
方法中。现在我们应该有一个看起来像这样的表单请求:
我们的控制器现在也应该看起来如下:
在我们的控制器中,我们将 store()
方法的第一个参数从\Illuminate\Http\Request
更改为我们新的 \App\Http\Requests\StoreUserRequest
。我们还通过将其提取到请求类中,降低了一些控制器方法的冗余。
注意:要让这个功能自动工作,你需要确保你的控制器正在使用
\Illuminate\Foundation\Auth\Access\AuthorizesRequests
和\Illuminate\Foundation\Validation\ValidatesRequests
traits。这些 traits 在 Laravel 新安装中为你提供的控制器中自动包含。所以,如果你正在扩展那个控制器,你就可以直接开始了。如果没有,请确保在你的控制器中包含这些特性。
将通用逻辑移动到 Action 或 Service
我们能采取的另一个步骤来清理 store()
方法,可能就是将我们的"业务逻辑"移出到一个单独的 action 或 service 类。在这个特定的使用案例中,我们可以看到 store()
方法的主要功能是创建一个用户,生成他们的头像,然后派发一个注册用户至新闻通讯的任务。在我个人看来,对于这个例子来说,action 或许比 service 更合适。我喜欢对于只做一件具体事情的小任务使用 action,而对于可能有几百行代码,并且需要做很多事情的大段代码,更适合用 service。
因此,让我们通过在我们的 app
文件夹中创建一个新的 Actions
文件夹,然后在这个文件夹中创建一个叫做 StoreUserAction.php
的新类来创建我们的 action。然后我们将代码移入 action 中,像这样:
现在我们可以更新我们的控制器来使用 action:
如你所看到的,我们现在能够将业务逻辑提取出控制器方法并放入 action 中。这是有用的,因为,正如我之前提到的,控制器本质上是我们请求和响应的"胶水"。所以,我们已经通过保持代码逻辑上的分离,减少了理解一个方法做了什么的认知负荷。例如,如果我们想检查授权或验证,我们知道应该查看表单请求。如果我们想检查请求数据做了什么,我们可以查看 action。
将代码抽象为这些单独的类的另一个巨大优势是,它可以使测试变得更容易,更快。
和 Action 一起使用 DTO
将你的业务逻辑抽象到服务和类中的另一个大的好处是,你现在可以在不同的地方使用这个逻辑,而无需复制你的代码。例如,假设我们有一个 UserController
处理传统的 web 请求,和一个 Api\UserController
处理 API 请求。为了方便论证,我们可以假设这些控制器的 store()
方法的一般结构将会是一样的。但是,如果我们的 API 请求并未使用 email
字段,而是使用 email_address
字段,我们该怎么办呢?我们将无法将我们的请求对象传递给 StoreUserAction
类,因为它会期望一个包含 email
字段的请求对象。
为了解决这个问题,我们可以使用 DTO(数据传输对象)。这是一种非常有用的方法,可以在你的系统代码中传递数据,而无需紧密地绑定到任何东西(在这种情况下,是请求)。
为了将数据传输对象添加到我们的项目中,我们将使用 Spatie 的 spatie/data-transfer-object 包,并使用以下 Artisan 命令进行安装:
composer require spatie/data-transfer-object
现在我们已经安装了这个包,让我们在 App
文件夹内创建一个新的 DataTransferObjects
文件夹,并创建一个新的 StoreUserDTO.php
类。然后需要确保我们的 DTO 继承了 Spatie\DataTransferObject\DataTransferObject
。然后我们可以像这样定义我们的三个字段:
现在我们已经完成了这个,我们可以在之前的 StoreUserRequest
中添加一个新的方法来创建并返回一个 StoreUserDTO
类,像这样:
我们现在可以更新我们的控制器,将 DTO 传递给 action 类:
最后,我们需要将 action 的方法更新,使其接受一个 DTO 作为参数,而不是请求对象:
通过这样做的结果是,我们现在已经完全将 action 的业务逻辑与请求对象解耦。这意味着我们可以在系统的多个地方重复使用此 action,而无须被特定请求结构所束缚。现在,我们也能够在 CLI 环境或者队列任务中使用这种方法,这些并不绑定到 web 请求。举个例子,如果我们的应用中有从 CSV 文件中导入用户的功能,我们可以从 CSV 数据中创建 DTO 并传入 action 中。
返回到原始问题,我们的 API 请求使用的是 email_address
而非 email
,我们现在可以通过简单地构建 DTO 并将 DTO 的 email
字段赋值为请求的 email_address
字段来解决这个问题。假设 API 请求有自己的单独的表单请求类。以这样的方式,它可能看起来像这样:
使用资源或者单一用途的控制器
保持控制器整洁的一个好方法是把它们变成"资源控制器"或者"单一用途的控制器"。再进一步更新我们例子中的控制器之前,我们先看一下这两个词都是什么意思。
资源控制器是一个基于具体资源提供功能的控制器。所以,在我们的案例中,我们的资源是 User
模型,我们希望在这个模型上执行所有的 CRUD(创建,读取,更新,删除)操作。资源控制器通常包括 index()
,create()
,store()
,show()
,edit()
,update()
和 destroy()
这些方法。它不一定要包含所有这些方法,但是它不会有这个名单之外的任何方法。通过使用这种类型的控制器,我们可以使我们的路由变为 RESTful。关于 REST 和 RESTful 的更多信息,你可以查看这篇文章。
而单一用途控制器是只有一个公共的 __invoke()
方法的控制器。如果你有一个控制器,并不符合我们在资源控制器中的 RESTful 方法,这些就非常有用了。
基于以上的信息,我们可以看到,通过将 unsubscribe
方法移动到它自己的单一用途控制器中,我们可以改进我们的 UserController
。这样做,我们就能让 UserController
成为一个只包含资源方法的资源控制器。
所以,让我们用以下的 Artisan 命令创建一个新的控制器:
php artisan make:controller UnsubscribeUserController -i
注意我们向命令传递了 -i
,这样新的控制器将会是显式的、单一用途的控制器。我们现在应该会有一个看起来如下的控制器:
现在我们可以移动方法的代码过去,并且从我们旧的控制器中删除 unsubscribe
方法:
最后记得切换 routes/web.php
文件中的路由,用 UnsubscribeController
来代替 UserController
的这个方法。
小结
本篇教程让你对如何清理 Laravel 项目的控制器有了一些了解。不过请记住,我在这里使用的技巧只是我个人的观点。我确定还有其他开发者会使用一种完全不同的方法来构建他们的控制器。最重要的部分是保持一致性,使用一种符合你(和你的团队)的工作流的方法。
No Comments