PHP - 使用 MVC 构建 Slim3 Web 应用程序并理解模型的作用

PHP - Structuring a Slim3 web application using MVC and understanding the role of the model

我正在尝试使用 Slim3 框架和 Twig 模板系统在 php 中创建一个身份验证系统,对于数据库,我正在使用 MySQL 和 PDO。我也在尝试使用模型视图控制器设计模式来实现它。但是,我很难理解如何将 MVC 结构用于 Web 应用程序。我在网上看了很多解释,似乎没有一个明确的答案。很多人说要使用 php 框架,例如 Laravel、Symfony 或 CodeIgniter,因为它们显然采用了类似 MVC 的结构。但是我宁愿保持简单并手动编写代码而不是使用框架。

目前我看到的 MVC 有两种解释。此图中描绘的第一个:

我看到的另一种解释是:(摘自this YouTube video

我已经做了研究。 this and this 等问题和答案很有帮助。但我仍然不确定如何构建我自己的应用程序,特别是识别和理解 MVC 的模型方面。我现在将解释我的身份验证应用程序的注册过程。所以你知道我的代码是如何工作的。

首先,我有一个 SQLQueries class,它只是将一系列 SQL 语句放入函数中。然后我有一个 SQLWrapper class,它具有可以在数据库中存储新用户详细信息等功能。此 class 还调用来自 SQL 查询 class 的函数。我还有一个 ValidateSanitize class,它具有清除用户输入以及检查用户输入在表单中是否有效的功能。我认为这三个 classes 是 MVC 模型方面的一部分,但我不确定。我看到很多其他教程使用“用户模型 class”,但我在我的应用程序中找不到对它的需求。

我的视图只是显示 html 的 Twig 模板,例如主页、注册、登录等。然后我有控制器。我打算有多个控制器来做不同的事情。现在我只实现了负责注册和登录用户的 AuthController。

因此,AuthController 做的第一件事是在名为 getRegisterForm 的函数中显示注册表单。用户提交表单后,postRegisterForm 函数会获取该用户输入并将其分配给受污染的变量。

public function postRegisterForm($request, $response)  
{
   $arr_tainted_params = $request->getParsedBody(); 

   $tainted_email = $arr_tainted_params['email'];  it a variable
   $tainted_username = $arr_tainted_params['username'];
   $tainted_password = $arr_tainted_params['password'];
   $tainted_password_confirm = $arr_tainted_params['password_confirm'];

接下来,所有前三个 classes 以及数据库详细信息都被实例化,因此它们的功能可以在 AuthController 中使用:

$sanitizer_validator = $this->container->ValidateSanitize;
$sql_wrapper = $this->container->SQLWrapper;
$sql_queries = $this->container->SQLQueries;
$db_handle = $this->container->get('dbase');

然后使用 sanitize_input 函数清除受污染的用户详细信息。然后将清理后的用户详细信息输入验证函数,以确保它们不会触发任何验证违规。密码也在此处散列:

$cleaned_email = $sanitizer_validator->sanitize_input($tainted_email, FILTER_SANITIZE_EMAIL); 
$cleaned_username = $sanitizer_validator->sanitize_input($tainted_username, FILTER_SANITIZE_STRING);
$cleaned_password = $sanitizer_validator->sanitize_input($tainted_password, FILTER_SANITIZE_STRING);
$cleaned_password_confirm = $sanitizer_validator->sanitize_input($tainted_password_confirm, FILTER_SANITIZE_STRING);

$hashed_cleaned_password = password_hash($cleaned_password, PASSWORD_DEFAULT); 

$sanitizer_validator->check_email_exists($cleaned_email);
$sanitizer_validator->validate_email($cleaned_email);
$sanitizer_validator->validate_username($cleaned_username);
$sanitizer_validator->validate_password($cleaned_password);
$sanitizer_validator→validate_password_confirm($cleaned_password_confirm);

最后有一个 if 语句检查所有验证错误消息是否为空。如果是,我们会向 SQLWrapper class 提供数据库详细信息以及 SQLQueries class 对象。然后,我们通过调用 SQLWrapper classes store-details 函数将用户详细信息插入到数据库中。最后我们将用户定向到登录页面,这样用户就可以登录到他们新注册的帐户。

if ($sanitizer_validator->get_validate_messages('email_error') == ' ' && $sanitizer_validator->get_validate_messages('username_error') == ' '
    && $sanitizer_validator->get_validate_messages('password_error') == ' ' && $sanitizer_validator->check_passwords_match($cleaned_password, $cleaned_password_confirm ) == true
    && $sanitizer_validator->check_email_exists($cleaned_email) == false)
{   

    $sql_wrapper->set_db_handle($db_handle); 
    $sql_wrapper->set_sql_queries($sql_queries); 

     $sql_wrapper->store_details($cleaned_email, $cleaned_username, $hashed_cleaned_password);
     return $response→withRedirect($this→container→router→pathFor('login'));

}

然而,如果任何验证错误消息不为空,那么我们调用 SanitiseValidate display_validate_messages,它只是将消息设置到会话中以显示在注册树枝模板上。然后我们重定向回注册页面,以便用户可以看到验证错误消息。

else
  {
      $sanitizer_validator->display_validate_messages();
      return $response->withRedirect($this->container->router->pathFor('register'));
  }
}

所以基于这个用户注册账号的结构。这是否遵循干净简单的 MVC 结构或是否需要进行一些更改?我的 classes 中的任何一个都扮演了模特的角色吗?任何关于我的结构的建议和技巧将不胜感激。

full application can be seen on my GitHub 如果有帮助的话。请注意,此版本比我在此问题中使用的示例代码稍旧。

事实上,关于如何在 Web 应用程序中应用 MVC 模式有多种方法。如此众多的变体是一个简单事实的结果,即最初的 MVC 模式——为桌面应用程序开发(由 Trygve Reenskaug,于 1979 年)——不能按原样应用于 Web 应用程序。 Here 是一个小说明。但是,从这组方法中,您可以选择最符合您要求的方法。也许您会在下定决心之前尝试更多。不过,在某些时候,您会知道哪一个符合您的愿景。

在下图中,我试图展示我在 Web MVC 工作流程上选择的方法 - 主要受到 Robert Martin's presentation Keynote: Architecture the Lost Years (licensed under a Creative Commons Attribution ShareAlike 3.0) 的启发。



一般来说,您可以认为 Web MVC 应用程序由以下部分组成:

  1. 领域模型(如模型,如模型层);
  2. 服务层(可选);
  3. 传递机制;
  4. 其他组件(如自己的库等)。

1) 域模型 应由以下组件组成:

  • 实体(例如域对象)和值对象。它们根据属性和行为对业务规则进行建模,并且独立于应用程序,可以被多种(类型)应用程序使用。
  • (Data) mappers and, optional, repositories。这些组件负责持久性逻辑。
  • 外部服务。它们用于执行涉及使用 external/own 库的不同任务(例如发送电子邮件、解析文档等)。

此外,域模型可以分为两部分:

a) 领域模型抽象 .这将是交付机制的组件或服务层的服务访问的模型层的唯一 space - 如果实现了一个:

  • 实体和值对象;
  • (数据)映射器抽象和可选的存储库抽象;
  • 外部服务的抽象。

    注意:我所说的抽象是指接口和抽象 类。

b) 领域模型实现 .这个 space 将是不同域模型抽象的实现(参见 a)所在的那个。依赖注入容器(作为交付机制的一部分)将负责将这些具体 类 的实例作为依赖项传递给应用程序的其他组件(例如控制器、视图、服务)等)。

2) 服务层(可选):从技术上讲,交付机制的组件可以直接与域的元素交互模型。尽管此类交互涉及(很多)操作,但仅特定于模型,而不特定于交付机制。因此,一个好的选择是将这些操作的执行推迟到service类(例如services),作为所谓service layer的一部分。交付机制组件将仅使用这些服务来访问域模型组件。

注:服务层实际上可以看作是模型层的一部分。在下面的图表中,我更喜欢将其显示为位于模型外部的图层。但是,在文件系统示例中,我将相应的文件夹放在域 space.

3) 交付机制 总结了用于确保用户与模型层组件之间交互的构造。我所说的用户不是指一个人,而是指一个人可以与之交互的界面——比如浏览器、控制台(例如 CLI)、桌面 GUI 等。

  • Web服务器:通过单点入口解析用户请求(index.php)。

  • 依赖注入容器:为应用程序的不同组件提供适当的依赖。

  • HTTP 消息(例如 HTTP 请求和 HTTP 响应)抽象(参见 PSR-7: HTTP message interfaces)。

  • Router:将请求组件(HTTP 方法和 URI 路径)与预定义列表中每个路由的组件(HTTP 方法和模式)进行匹配路由和 returns 匹配的路由,如果找到的话。

  • Front controller: 将用户请求匹配到一个route,并派发给某个controller and/or view action.

  • 控制器。他们写入(例如执行创建、更新和删除操作)模型层并且(应该)不期望任何结果。这可以通过直接与域模型中定义的组件交互来实现,或者最好只与服务交互 类.

  • 观看次数。它们应该是 类,而不是模板文件。他们可以接收模板引擎作为依赖项。它们只从模型层获取数据(例如执行读取操作)。通过直接与域模型中定义的组件交互,或者最好只与服务交互 类。此外,它们还决定向用户显示哪个结果(如字符串)或模板文件内容。视图操作应始终 return 一个 HTTP 响应对象(可能由 PSR-7 规范定义),其主体将预先用提到的结果或模板文件内容更新。

  • 模板文件。应尽可能保持简单。整个表示逻辑应该只发生在视图实例中。因此,模板文件应该只包含变量(可以是纯 PHP 变量,也可以是使用模板引擎语法提供的变量),也许还有一些简单的条件语句或循环。

  • Response emitter: 读取 HTTP 响应实例的主体 return 并打印出来。

4) 其他成分。如愿。比如自己开发的一些库。就像 PSR-7 抽象的实现。


我如何选择发送用户请求:

正如您在上图中看到的,前端控制器不仅将用户请求分派给控制器操作(为了更新域模型),而且还分派给视图操作(为了读取和显示从模型层更新 state/data)。一种分裂的派遣。这可以通过将控制器动作和视图动作分配给每个路由(如下所示)并告诉前端控制器依次调用它们来相对容易地实现:

<?php

use MyApp\UI\Web\Application\View;
use MyApp\UI\Web\Application\Controller;

// Note: $this specifies a RouteCollection to which the route is added. 
$this->post('/upload', [
    'controller' => [Controller\Upload::class, 'uploadFiles'],
    'view' => [View\Upload::class, 'uploadFiles'],
]);

这种方法在用户请求调度方面提供了灵活性。例如,视图动作的名称可以与控制器动作的名称不同。或者,为了只获取模型层数据,您不需要将用户请求分派到控制器,而只需分派到视图。因此,您根本不需要在路由中分配控制器操作:

<?php

use MyApp\UI\Web\Application\View;

$this->get('/upload', [View\Upload::class, 'listFiles']);

文件系统结构示例:

myapp/domain:包含域模型 类 和服务的文件夹。该目录可以放入 "myapp/web/src" 文件夹,但不应该,因为模型层和服务层不是交付机制的一部分。

myapp/web:包含传送机制的文件夹类。它的名称描述了应用程序的类型 - 可以是 Web 应用程序、CLI 应用程序等。

myapp/web/src:


资源:

*) Sandro Mancuso : An introduction to interaction-driven design

*) 我的 中列出的那些。

*) Alejandro Gervasio 提供的教程:

*) Slim 3 页面上的示例:Action-Domain-Responder with Slim.

有一门课程让您逐步了解如何使用 slim 3 制作 MVC。我会 link 在这里:https://codecourse.com/courses/slim-3-authentication。希望这对您有所帮助,这是一门非常容易上手的课程,您会学到很多东西。