十一、提取动作逻辑到控制器

到目前为止,我们已经提取了模型领域逻辑和视图表示逻辑。我们的页面脚本中只剩下两种逻辑:

  • 依赖关系逻辑,它使用应用程序设置创建对象
  • 使用这些对象执行页面操作的操作逻辑(有时称为业务逻辑)

在本章中,我们将从页面脚本中提取一层Controller类。这些将分别处理遗留应用程序中的剩余操作逻辑和依赖项创建逻辑。

嵌入式动作逻辑

对于嵌入式动作逻辑与依赖逻辑混合的示例,我们可以查看附录 G中最后一章的结束示例代码,响应视图文件后的代码。其中,我们做了一些设置工作,然后检查一些条件并调用我们域的不同部分Transactions,最后我们放在一起一个Response对象,将我们的响应发送给客户端。

与混合呈现逻辑的问题一样,我们无法将动作逻辑与页面脚本的其余部分分开测试。类似地,我们无法轻松地更改依赖项创建逻辑以使页面脚本更易于测试。

我们解决了嵌入式操作逻辑的问题,就像我们解决了嵌入式表示逻辑一样。我们必须将动作代码提取到它自己的类中,以分离页面脚本的各种剩余关注点。这还允许我们独立于应用程序的其余部分来测试操作逻辑。

提取过程

现在,从页面脚本中提取动作逻辑对我们来说应该是一项相对容易的任务。因为域层和表示层已经被提取,所以动作逻辑应该是显而易见的。这项工作本身仍然值得关注细节,因为主要问题将是从操作逻辑本身中分离依赖项设置部分。

一般来说,流程如下:

  1. 查找一个页面脚本,其中操作逻辑仍然与代码的其余部分混合在一起。
  2. 在该页面脚本中,重新排列代码,使所有操作逻辑都位于其自己的中心块中。抽查重新排列的代码,确保其仍然正常工作。
  3. 将动作逻辑的中心块提取到一个新的Controller类,并修改页面脚本以使用新的Controller。在控制器就位的情况下抽查页面脚本。
  4. 为新的Controller类编写单元测试并再次抽查。
  5. 提交新代码和测试,将它们推送到公共存储库,并通知 QA。
  6. 找到另一个嵌入动作逻辑的页面脚本,然后重新启动;当所有页面脚本使用Controller对象时,我们就完成了。

搜索嵌入式动作逻辑

此时,我们应该能够找到操作逻辑,而不必使用我们的项目范围的搜索工具。我们的遗留应用程序中的每个页面脚本可能至少还有一点操作逻辑。

重新整理页面脚本,抽查

当我们有一个 Tyl T1 的候选页面脚本时,我们继续进行 TytT2 重排代码,以便所有的设置和依赖创建工作都在顶部,所有的动作逻辑都在中间,并且,Ty0 T0 的调用在底部。对于我们在这里的开始示例,我们将使用上一章末尾的代码,如附录 G响应视图文件后的代码中所示。

识别代码块

首先,我们进入脚本的顶部,在第一行(或者在包含设置脚本之后)放置/* DEPENDENCY */注释。然后我们转到脚本的最后一行,$response->send()行,并在上面放置/* FINISHED */注释。

现在我们到了一个点我们必须运用我们的专业判断。在页面脚本中的设置和依赖项工作完成后的某一行,我们将看到代码开始执行某种操作逻辑。由于操作逻辑和设置逻辑可能仍然交织在一起,因此我们对这种转换发生在何处的评估可能有些武断。即便如此,我们必须选择一个我们认为行动逻辑真正开始的点,并在那里发表评论。

将代码移动到其相关块

一旦在页面脚本中识别出这三个块,我们就开始重新排列代码,以便在/* DEPENDENCY *//* CONTROLLER */之间只进行设置和依赖项创建工作,在/* CONTROLLER *//* FINISHED */之间只进行操作逻辑。

通常,我们应该避免依赖项块中的条件或循环,并避免在控制器块中创建对象。依赖项块中的代码应仅创建对象,控制器块中的代码应仅对依赖项块中已创建的对象进行操作。

鉴于我们在附录 G中的起始代码、响应视图文件后的代码,我们可以在附录 H控制器重新排列后的代码中看到示例重新排列的结果。值得注意的是,我们将$user_id声明向下移动到控制器块,并将Response对象创建向上移动到依赖项块。中央控制器块中的原始动作逻辑保持不变。

抽查重新排列的代码

最后,在重新安排页面脚本后,我们需要抽查我们的更改,以确保一切仍然正常工作。如果我们有表征测试,我们应该运行这些测试。否则,我们应该浏览或以其他方式调用页面脚本。如果它不能正常工作,我们需要撤销并重新安排,以便修复引入的任何错误。

当我们的抽查成功后,我们可能希望提交到目前为止的更改。这将为我们提供一个已知的工作状态,如果将来的更改出错,我们可以恢复到该状态。

提取一个控制器类

现在我们有了一个正常工作的重新排列的页面脚本,我们可以将中央控制器块提取到它自己的类中。这并不困难,但我们将分几个子步骤来确保一切顺利进行。

选择一个类名

在提取到类之前,我们需要为要提取到的类选择一个名称。

对于我们的域层类,我们选择了顶级名称空间。因为这是一个控制器层,所以我们将使用顶级名称空间控制器。我们使用的名称空间不如为所有控制器一致使用相同的名称空间重要。就个人而言,我更喜欢控制器,因为它足够广泛,可以包含不同类型的控制器,例如应用程序控制器。

该名称空间中的类名应反映页面脚本在 URL 层次结构中的位置,在路径中有目录分隔符的位置使用名称空间分隔符。这种方法使原始页面脚本目录路径变得显而易见,并使子目录在类结构中保持良好的组织。我们还用Page作为类名的后缀,表示它是一个页面控制器。

例如,如果页面脚本位于/foo/bar/baz.php,则类名应为Controller\Foo\Bar\BazPage。然后,类文件本身将被放置在classes/Controller/Foo/Bar/BazPage.php下的中心类目录中。

创建骨架类文件

一旦我们有了一个类名称,我们就可以为它创建一个骨架类文件。我们在后面添加了两个空方法作为占位符:__invoke()方法将从页面脚本接收操作逻辑,构造函数最终将接收类的依赖项。

classes/Controller/Foo/Bar/BazPage.php
1 <?php
2 namespace Controller\Foo\Bar;
3
4 class BazPage
5 {
6 public function __construct()
7 {
8 }
9
10 public function __invoke()
11 {
12 }
13 }
14 ?>

为什么调用()?

就个人而言,我喜欢为此选择__invoke()魔术方法,但您可能希望使用exec()或其他适当的术语来表示我们正在执行或以其他方式运行控制器。无论我们选择什么方法名称,我们都应该始终如一地使用它。

移动动作逻辑,抽查

现在我们准备将动作逻辑提取到新的Controller类中。

首先,我们从页面脚本中剪切控制器块,并按原样将其粘贴到__invoke()方法中。我们在动作逻辑的末尾return $response添加一行,将响应对象发送回调用代码。

接下来,我们回到页面脚本。在提取的动作逻辑的位置,我们创建一个新的Controller实例并调用其__invoke()方法,得到一个响应对象。

在我们所有的页面脚本中,always应该为控制器对象使用相同的变量名。这里的所有示例都将使用名称$controller。这并不是因为名称$controller很特殊,而是因为在后面的章节中,这种一致性将非常重要。

此时,我们已经成功地将动作逻辑与页面脚本解耦。然而,这种解耦从根本上打破了动作逻辑,因为控制器依赖于页面脚本中的变量。

考虑到这一点,我们开始抽查和修改周期。我们浏览或以其他方式调用页面脚本,发现某个特定变量对控制器不可用。我们将其添加到__invoke()方法签名中,并再次抽查。我们继续在__invoke()方法中添加变量,直到控制器拥有所需的一切,并且我们的抽查运行完全成功。

对于流程的这一部分,最好设置error_reporting(E_ALL)。这样,我们将为动作逻辑中的每个未初始化变量获得一个 PHP 通知。

鉴于我们在附录 H中重新排列的页面脚本,控制器重新排列后的代码,我们最初提取到控制器的结果可以在附录 I控制器提取后的代码中看到。结果表明,提取的动作逻辑需要四个变量:$request$response$user$article_transactions

将控制器转换为依赖注入和抽查

一旦我们在__invoke()方法中有了动作逻辑的工作块,我们将把方法参数转换成构造函数参数,以便控制器可以使用依赖注入。

首先,我们剪切__invoke()参数,并将它们作为一个整体粘贴到__construct()参数中。然后我们编辑类定义和__construct()方法,将参数保留为属性。

接下来,我们修改__invoke()方法以使用类属性而不是方法参数。这意味着在每个需要的变量前面加上$this->

然后,我们回到页面脚本。我们剪切__invoke()调用的参数,并将它们粘贴到控制器实例化中。

现在我们已经将控制器转换为依赖注入,我们需要再次抽查页面脚本以确保一切正常。如果没有,我们需要撤销并重新进行转换,直到测试通过。

此时,我们可以删除/* DEPENDENCY *//* CONTROLLER *//* FINISHED */注释。它们已经达到了目的,不再需要了。

鉴于附录 I中的__invoke()用法,控制器提取后的代码,我们可以在附录 J中看到将控制器转换为依赖注入时的样子,控制器依赖注入后的代码。我们已经将控制器__invoke()参数移动到__construct(),保留为属性,在__invoke()方法体中使用新属性,并修改页面脚本以在new时间而不是__invoke()时间传递所需的变量。

一旦我们有了一个工作页面脚本,我们可能希望再次提交我们的工作,以便我们有一个已知的正确状态,如果需要的话,我们可以稍后恢复到该状态。

编写控制器测试

尽管我们已经测试了我们的页面脚本,但我们需要为我们提取的控制器逻辑编写一个单元测试。当我们编写测试时,我们需要将所有需要的依赖项注入到我们的控制器中,最好将测试加倍,例如伪造或模拟,以便我们可以将控制器与系统的其余部分隔离。

当我们做出断言时,它们可能应该反对从__invoke()方法返回的响应对象。我们可以使用getView()来确保设置了右视图文件,getVars()来检查要在视图中使用的变量,getLastCall()来查看是否正确设置了最终可调用(如果有)。

提交、推送、通知 QA

一旦通过了单元测试,并且原始页面脚本的测试也通过了,我们就可以提交新代码和测试了。然后我们推到公共存储库并通知 QA 我们已经准备好让他们审查我们的工作。

做。。。虽然

现在我们继续下一页脚本,它嵌入了动作逻辑,并重新开始提取过程。当我们所有的页面脚本都使用依赖注入的控制器对象时,我们就完成了。

常见问题

我们可以将参数传递给控制器方法吗?

在示例中,我们从__invoke()方法中删除了所有参数。但是,有时我们希望将参数作为控制器逻辑的最后一分钟信息传递给该方法。

总的来说,我们在现代化进程的这个时候应该避免这样做。这并不是因为这是一种糟糕的做法,而是因为我们需要在控制器调用中保持非常高的一致性,以便在以后的现代化步骤中实现。最一致的是根本没有__invoke()参数。

如果我们需要向控制器传递额外信息,我们应该通过构造函数来传递。当我们传递请求值时尤其如此。

例如,与此相反:

page_script.php
1 <?php
2 /* DEPENDENCY */
3 // ...
4 $response = new \Mlaphp\Response('/path/to/app/views');
5 $foo_transactions = new \Domain\Foo\FooTransactions(...);
6 $controller = new \Controller\Foo(
7 $response,
8 $foo_transactions
9 );
10
11 /* CONTROLLER */
12 $response = $controller->__invoke('update', $_POST['user_id']);
13
14 /* FINISHED */
15 $response->send();
16 ?>

我们可以这样做:

page_script.php
1 <?php
2 /* DEPENDENCY */
3 // ...
4 $response = new \Mlaphp\Response('/path/to/app/views');
5 $foo_transactions = new \Domain\Foo\FooTransactions(...);
6 $request = new \Mlaphp\Request($GLOBALS);
7 $controller = new \Controller\Foo(
8 $response,
9 $foo_transactions,
10 $request
11 );
12
13 /* CONTROLLER */
14 $response = $controller->__invoke();
15
16 /* FINISHED */
17 $response->send();
18 ?>

__invoke()方法体将使用$this->request->get['item_id']

一个控制器可以有多个动作吗?

在示例中,我们的控制器对象执行单个动作。然而,页面控制器通常包含多个操作,例如插入和更新数据库记录。

我们从页面脚本中提取操作逻辑的第一步应该保持代码几乎完好无损,允许使用属性而不是局部变量等等。但是,一旦代码在类中,将逻辑拆分为单独的操作方法是完全合理的。然后__invoke()方法可以变成一个选择正确操作方法的switch语句。如果我们这样做,我们应该确保更新我们的控制器测试,并继续抽查页面脚本,以确保我们的更改不会破坏任何内容。

注意,如果我们创建额外的控制器动作方法,我们需要避免从页面脚本调用它们。为了后续现代化步骤中所需的一致性,__invoke()方法应该是页面脚本在其控制器块中调用的唯一控制器方法。

如果控制器包含 include 调用怎么办?

不幸的是,当我们开始重新安排页面脚本时,我们可能会发现控制器块中仍然有几个include调用。(出于设置和依赖性目的而调用include并不是什么大事,尤其是在每个页面脚本中都是相同的情况下。)

在控制器块中具有include调用是传统应用程序开始时所采用的面向包含的体系结构的产物。这是一个特别难解决的问题。我们希望将动作逻辑封装在类中,而不是在我们include执行动作的瞬间将其封装在文件中。

现在,我们必须接受这样的想法:页面脚本的控制器块中的include调用虽然丑陋,但却是必要的。如果需要,我们应该转移视线,将它们与页面脚本中的其余控制器代码一起复制到Controller类中。

作为安慰,我们将在下一章中解决这些嵌入式include调用的问题。

回顾和下一步

将动作逻辑提取到控制器层完成了我们遗留应用程序的巨大现代化目标。我们现在有了一个完整的模型-视图-控制器系统:一个用于模型的域层,一个用于视图的表示层,以及一个连接两者的控制器层。

我们应该对我们的现代化进程感到非常满意。保留在每个页面脚本中的代码是其原始自身的影子。大多数逻辑是创建具有依赖关系的控制器的接线代码。其余的逻辑在所有页面脚本中都是相同的;调用控制器并发送返回的响应对象。

然而,有一个重要的遗留工件需要我们处理。为了完成控制器逻辑的完整提取和封装,我们需要删除嵌入在我们的控制器类中的任何剩余include调用。