【问题标题】:PHP - Structuring a Slim3 web application using MVC and understanding the role of the modelPHP - 使用 MVC 构建 Slim3 Web 应用程序并了解模型的作用
【发布时间】:2019-01-14 12:01:45
【问题描述】:

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

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

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

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

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

我的视图只是显示 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'];

接下来所有三个前面的类以及数据库详细信息都被实例化,因此它们的功能可以在 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 类提供数据库详细信息以及 SQLQueries 类对象。然后,我们通过调用 SQLWrapper 类的 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 结构或是否需要进行一些更改?我的任何课程都扮演了模特的角色吗?任何有关我的结构的建议和提示将不胜感激。

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

【问题讨论】:

  • 我认为您在这里错过了@tereško 回答的重点:stackoverflow.com/questions/5863870/…。仔细阅读并特别注意“什么是模型”部分。您的控制器中到处都有域逻辑,更糟糕的是,您在整个过程中都充满了 SQL/映射逻辑。此外,Model 文件夹中的文件本身就是类,但它们并没有最好地遵守 OOP 原则
  • @jeremy 这就是我不知道我的应用程序应该去哪里的问题。尽管阅读了那个答案,但我仍然不明白这就是我要问的原因。你说我到处都有领域逻辑。我不知道那是什么意思。一个告诉我如何正确构建它的答案是我所追求的。而且它不是重复的。
  • @jeremy 我也不确定你所说的模型文件夹是什么意思。你是说 src 文件夹吗?
  • 我说的是那个文件夹,抱歉。

标签: php model-view-controller design-patterns model slim-3


【解决方案1】:

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

在下面的图表中,我试图展示我在 Web MVC 工作流中选择的方法 - 主要受Robert Martin 的演示文稿Keynote: Architecture the Lost Years 的启发(在Creative Commons Attribution ShareAlike 3.0 下获得许可)。



通常,您可以将 Web MVC 应用程序视为由以下部分组成:

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

1) 领域模型应包含以下组件:

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 前端控制器:将用户请求与路由匹配,并将其分派到某个控制器和/或视图操作。

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

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

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

  • 响应发射器:读取视图返回的 HTTP 响应实例的主体并打印出来。

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


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

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

<?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

*) 我的older answer 中列出的那些。

*) Alejandro Gervasio 提供的教程:

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

【讨论】:

  • 我很喜欢这个答案,但说实话,当我开始时,我更加困惑。我将不得不对此进行深入调查,因为这是我见过的最复杂的事情。也许阅读这些资源会对我有所启发。谢谢。
  • 嘿,我有一个关于域模型抽象和域模型实现之间的区别的问题。在 Alejandro Gervasio 的示例中,这是否对应于他的接口是域模型抽象,而这些接口的实际实现是域模型实现?当我慢慢开始理解这一点时,我可能还有一些问题。再次感谢。
  • @GR412 是的,你可以把他的接口想象成一个领域模型抽象空间的一部分。并将它们的实现作为 域模型实现 空间的一部分。请注意,域模型抽象这个名称只是我的一个选择。通过选择它,我想强调一个事实,即交付机制应该仅通过该空间的构造与模型层“通信”。
  • @GR412 虽然,显然,我的空间领域模型抽象不仅包含数据映射器、存储库和外部服务的抽象(例如接口和抽象类),还包含实现(例如具体类) - 实体。如果您愿意,您也可以为空间领域模型抽象 中的实体定义抽象。并将他们的实现放在 领域模型实现 空间中。
  • @ESt3b4n 更准确地说:“服务也有业务逻辑”...实体不仅由它们的属性定义,而且由它们的行为定义b>(业务逻辑)。而且,由于它们是不可更改的,它们的业务规则并不特定于特定的应用程序。这就是为什么它们可以在应用程序之间共享。服务也有业务逻辑。不过,相比之下,它们的业务规则是特定于特定应用程序的。
【解决方案2】:

有一个课程,您可以通过 slim 3 制作 MVC。我将在此处链接:https://codecourse.com/courses/slim-3-authentication。希望这对您有所帮助,这是一门非常容易学习的课程,并且您会学到很多东西。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-06
    • 1970-01-01
    • 1970-01-01
    • 2010-12-07
    • 1970-01-01
    • 1970-01-01
    • 2013-04-18
    • 2019-05-27
    相关资源
    最近更新 更多