MVC 中的模型应该如何构建? [关闭]

2023-12-11

我刚刚掌握 MVC 框架,经常想知道模型中应该包含多少代码。我倾向于拥有一个具有如下方法的数据访问类:

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

我的模型往往是映射到数据库表的实体类。

模型对象是否应该具有所有数据库映射属性以及上面的代码,或者是否可以将该代码与数据库实际工作的代码分开?

我最终会有四层吗?


Disclaimer: the following is a description of how I understand MVC-like patterns in the context of PHP-based web applications. All the external links that are used in the content are there to explain terms and concepts, and not to imply my own credibility on the subject.

我首先要明确的是:模型是一层.

第二:有区别经典MVC以及我们在网络开发中使用的内容。Here's我写的一个旧答案,它简要描述了它们的不同之处。

模型不是什么:

该模型不是一个类或任何单个对象。这是一个很常见的错误(我也这样做了,尽管最初的答案是在我开始学习其他知识时写的),因为大多数框架都会延续这种误解。

它既不是对象关系映射技术(ORM),也不是数据库表的抽象。任何告诉你其他情况的人很可能试图'sell'另一个全新的 ORM 或整个框架。

什么是模型:

在正确的 MVC 适配中,M 包含所有领域业务逻辑和模型层 is mostly由三种类型的结构组成:

  • 领域对象

    领域对象是纯领域信息的逻辑容器;它通常代表问题域空间中的逻辑实体。通常称为商业逻辑.

    您可以在此处定义如何在发送发票之前验证数据或计算订单的总成本。同时,领域对象完全不知道存储 - 都不知道where(SQL 数据库、REST API、文本文件等)甚至也不是if它们被保存或检索。

  • 数据映射器

    这些对象只负责存储。如果您将信息存储在数据库中,那么这就是 SQL 所在的位置。或者您可能使用 XML 文件来存储数据,并且您的数据映射器正在解析 XML 文件和解析 XML 文件。

  • Services

    您可以将它们视为“更高级别的域对象”,但不是业务逻辑,Services负责之间的交互领域对象 and Mappers。这些结构最终创建一个“公共”接口,用于与域业务逻辑交互。您可以避免它们,但代价是会泄漏一些域逻辑控制器.

    这个问题有一个相关的答案ACL实施问题 - 它可能有用。

模型层和 MVC 三元组其他部分之间的通信只能通过Services。明确的分离还有一些额外的好处:

  • 它有助于执行单一责任原则 (SRP)
  • 提供额外的“回旋余地”以防逻辑发生变化
  • 使控制器尽可能简单
  • 如果您需要外部 API,则提供清晰的蓝图

 

如何与模特互动?

Prerequisites: watch lectures "Global State and Singletons" and "Don't Look For Things!" from the Clean Code Talks.

获取对服务实例的访问

对于这两个View and 控制器实例(您可以称之为:“UI 层”)来访问这些服务,有两种通用方法:

  1. 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用 DI 容器。
  2. 使用服务工厂作为所有视图和控制器的强制依赖项。

正如您可能怀疑的那样,DI 容器是一个更优雅的解决方案(但对于初学者来说并不是最简单的)。我建议考虑此功能的两个库是 Syfmony 的独立库依赖注入组件 or Auryn.

使用工厂和 DI 容器的解决方案都可以让您共享各种服务器的实例,以便在给定的请求响应周期的选定控制器和视图之间共享。

模型状态的改变

现在您可以访问控制器中的模型层,您需要开始实际使用它们:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

您的控制器有一个非常明确的任务:获取用户输入,并根据此输入更改业务逻辑的当前状态。在此示例中,在“匿名用户”和“登录用户”之间更改的状态。

控制器不负责验证用户的输入,因为这是业务规则的一部分,并且控制器绝对不会调用 SQL 查询,就像您所看到的那样here or here(请不要讨厌他们,他们是被误导的,而不是邪恶的)。

向用户显示状态更改。

好的,用户已登录(或失败)。怎么办?该用户仍不知情。所以你需要实际产生一个响应,这是视图的责任。

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

在这种情况下,视图根据模型层的当前状态生成两种可能的响应之一。对于不同的用例,您将让视图根据“当前选择的文章”之类的内容选择不同的模板进行渲染。

表示层实际上可以变得非常复杂,如下所述:了解 PHP 中的 MVC 视图.

但我只是在制作一个 REST API!

当然,在某些情况下,这有点矫枉过正。

MVC只是一个具体的解决方案关注点分离原则。MVC 将用户界面与业务逻辑分开,并且在 UI 中将用户输入的处理和表示分开。这一点至关重要。虽然人们经常将其描述为“三合会”,但它实际上并不是由三个独立的部分组成。结构更像是这样的:

MVC separation

这意味着,当表示层的逻辑几乎不存在时,务实的方法是将它们保留为单层。它还可以大大简化模型层的某些方面。

使用这种方法,登录示例(对于 API)可以写为:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $data = [
        'status' => 'ok',
    ];
    try {
        $identity = $this->identification->findIdentityByEmailAddress($email);
        $token = $this->identification->loginWithPassword(
            $identity,
            $request->get('password')
        );
    } catch (FailedIdentification $exception) {
        $data = [
            'status' => 'error',
            'message' => 'Login failed!',
        ]
    }

    return new JsonResponse($data);
}

虽然这是不可持续的,但当您有复杂的逻辑来渲染响应主体时,这种简化对于更琐碎的场景非常有用。但被警告,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。

 

如何建立模型?

由于没有单个“模型”类(如上所述),因此您实际上并没有“构建模型”。相反,你从制作开始Services,它们能够执行某些方法。然后实施领域对象 and Mappers.

服务方法示例:

在上述两种方法中,都有这种用于身份识别服务的登录方法。它实际上会是什么样子。我正在使用相同功能的稍微修改版本图书馆,我写的..因为我很懒:

public function loginWithPassword(Identity $identity, string $password): string
{
    if ($identity->matchPassword($password) === false) {
        $this->logWrongPasswordNotice($identity, [
            'email' => $identity->getEmailAddress(),
            'key' => $password, // this is the wrong password
        ]);

        throw new PasswordMismatch;
    }

    $identity->setPassword($password);
    $this->updateIdentityOnUse($identity);
    $cookie = $this->createCookieIdentity($identity);

    $this->logger->info('login successful', [
        'input' => [
            'email' => $identity->getEmailAddress(),
        ],
        'user' => [
            'account' => $identity->getAccountId(),
            'identity' => $identity->getId(),
        ],
    ]);

    return $cookie->getToken();
}

正如您所看到的,在这个抽象级别,没有任何迹象表明数据是从哪里获取的。它可能是一个数据库,但也可能只是一个用于测试目的的模拟对象。即使是实际使用的数据映射器也隐藏在private这项服务的方法。

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

创建映射器的方法

要实现持久性的抽象,最灵活的方法是创建自定义数据映射器.

Mapper diagram

From: PoEAA book

在实践中,它们是为了与特定类或超类交互而实现的。假设你有Customer and Admin在你的代码中(都继承自User超类)。两者最终可能都会有一个单独的匹配映射器,因为它们包含不同的字段。但您最终也会得到共享和常用的操作。例如:更新“最后一次在线看到”时间。更实用的方法是拥有一个通用的“用户映射器”,它只更新时间戳,而不是让现有的映射器更加复杂。

一些补充意见:

  1. 数据库表和模型

    虽然有时数据库表之间存在直接的 1:1:1 关系,域对象, and Mapper,在较大的项目中,它可能比您预期的要少见:

    • 单个人使用的信息域对象可能是从不同的表映射的,而对象本身在数据库中没有持久性。

      Example:如果您要生成月度报告。这将从不同的表收集信息,但没有神奇的MonthlyReport数据库中的表。

    • 单个Mapper可以影响多个表。

      Example:当您存储数据时User对象,这个域对象可以包含其他域对象的集合 -Group实例。如果您更改它们并存储User, the 数据映射器必须在多个表中更新和/或插入条目。

    • 数据来自单个域对象存储在多个表中。

      Example:在大型系统中(例如:中型社交网络),将用户身份验证数据和经常访问的数据与较大的内容块分开存储可能是务实的,但很少需要这样做。在这种情况下,您可能仍然有一个User类,但它包含的信息取决于是否获取完整的详细信息。

    • 对于每一个域对象可以有多个映射器

      Example:您有一个新闻网站,该网站具有面向公众的共享代码和管理软件。但是,虽然两个接口使用相同的Article类,管理层需要在其中填充更多信息。在这种情况下,您将有两个单独的映射器:“内部”和“外部”。每个执行不同的查询,甚至使用不同的数据库(如在主数据库或从数据库中)。

  2. 视图不是模板

    ViewMVC 中的实例(如果您没有使用该模式的 MVP 变体)负责表示逻辑。这意味着每个View通常会同时使用至少几个模板。它从以下位置获取数据模型层然后,根据收到的信息,选择模板并设置值。

    您从中获得的好处之一是可重用性。如果您创建一个ListView类,然后,通过编写良好的代码,您可以让同一个类处理文章下面的用户列表和评论的表示。因为它们都有相同的呈现逻辑。您只需切换模板即可。

    您可以使用原生 PHP 模板或者使用一些第三方模板引擎。可能还有一些第三方库,它们可以完全替代View实例。

  3. 旧版本的答案怎么样?

    唯一的重大变化是,所谓的Model在旧版本中,实际上是Service。 “图书馆类比”的其余部分保持得很好。

    我看到的唯一缺陷是,这将是一个非常奇怪的库,因为它会返回书中的信息,但不会让你接触书本身,因为否则抽象就会开始“泄漏”。我可能不得不想一个更合适的类比。

  4. 之间有什么关系View and 控制器实例?

    MVC结构由两层组成:ui层和model层。区内主要建筑物UI layer是视图和控制器。

    当您处理使用 MVC 设计模式的网站时,最好的方法是在视图和控制器之间建立 1:1 的关系。每个视图代表网站中的整个页面,并且它有一个专用控制器来处理该特定视图的所有传入请求。

    例如,要表示一篇打开的文章,您需要\Application\Controller\Document and \Application\View\Document。这将包含 UI 层处理文章时的所有主要功能(当然你可能有一些XHR与文章不直接相关的组件).

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

MVC 中的模型应该如何构建? [关闭] 的相关文章

随机推荐

  • Google Chrome 扩展的 onBrowserClose 事件?

    我正在为 Google Chrome 开发一个扩展 我的后台脚本每次都会在使用 XMPP API 的服务器上进行授权 并订阅 PubSub 节点 我需要在退出时取消订阅 否则虚拟订阅将保留在服务器上 有没有onBrowserCloseGoo
  • 带有 pabot 的机器人框架:是否可以在两个测试中将两个不同的值传递给变量

    例子 我有file1 robot and file2 robot并且每个都有 var 作为变量 我可以将两个不同的值传递给这个值吗 var 在命令行中 就像是pabot v var one two file1 robot file2 rob
  • 经典 ASP 日期之间的工作日数

    经典 ASP VBScript 中有没有办法获取两个日期之间的工作日数 显然 我们有DateDiff 函数 但这会拉回总天数 但我想省略周末 你说得对 DateDiff 不包括这一点 但它可以与WeekDay 计算出是否Day适逢周末 通过
  • 如何对发送到服务器的http请求进行加密?

    是否可以隐藏 或加密 HTTP 请求的内容 以便授权人员之外的其他人无法查看它 例如 如果一个用户只是在登录页面中提交数据 即使使用 http post 那么也可以在 firebug 类工具中看到请求标头中包含的用户名和密码等内容 我知道客
  • 条件 DB2 SQL 查询

    假设我有一个名为 Company 的表 其键为 CompanyID 还有另一个名为 CompanyAddress 的相关表 它具有 CompanyID 外键 因此可以轻松建立连接 此 CompanyAddress 表可以具有给定公司的多个地
  • 从 cs 文件加载脚本并访问主机方法、属性等?

    我只是在和罗斯林玩 但不确定如何执行以下操作 为了保持简单 假设我有一个主机程序 它有一个像这样的方法 public void DisplayMessage string message MessageBox Show message 然后
  • 确定数组是否包含 n...n+m 的算法?

    我在 Reddit 上看到这个问题 但没有给出积极的解决方案 我认为在这里问这个问题是一个完美的问题 这是关于面试问题的帖子 编写一个方法 该方法接受大小为 m 的 int 数组 如果该数组包含数字 n n m 1 该范围内的所有数字以及仅
  • 实现自定义配置节处理程序

    从各种来源 包括 stackOverlflow 收集的信息 但是当我开始使用它时 我收到以下错误消息 配置属性 deviceconfig 可能不是从 ConfigurationSection 派生的 我现在已经在这个问题上挣扎了一整天 而且
  • 在java程序中执行bash命令

    自从我寻找以来已经有一段时间了 但我没有找到解决方案 我正在尝试在 Linux 上的 jar 文件中执行 bash 命令 为此 我尝试了很多方法 包括 Process p new ProcessBuilder java jar M1 MIA
  • 当变量值丢失时,Django 模板中的 Javascript 语法错误

    我有一个带有 AJAX 菜单的 Django 模板 单击不同的菜单项会重新加载页面的一部分 每次菜单单击都会通过 AJAX 调用 Django 视图函数并返回一些要在页面上显示的数据 我只加载主页中所有菜单项所需的所有 JS 据我所知 AJ
  • SerialPort.Read(....) 不尊重 ReadTimeOut

    与支付终端通信的一些旧代码中存在错误 在新的付款开始之前 代码会尝试清除串行端口的内部读取缓冲区 我将代码削减到最低限度 它使用 NET SerialPort 类型 设置读取超时为 50ms 然后它读取 512 字节并继续这样做 直到不再读
  • 是否可以在 C# 中返回对变量的引用? [复制]

    这个问题在这里已经有答案了 例如 我可以返回对双精度值的引用吗 这就是我想做的 ref double GetElement Calculate x y z return ref doubleArray x y z 像这样使用它 void f
  • socket.io 的 C 客户端

    我正在尝试使用socket io 建立从C 程序到节点服务器的连接 我能想到的唯一方法是从 C 程序向节点服务器发出 http 请求 所以我已经对此部分进行了一些介绍 现在我需要从C程序接收来自节点服务器的一些信息 是否有任何 c 客户端库
  • Python 凯撒密码解码器

    在我的课程中 我的任务是创建一个凯撒密码解码器 该解码器接受输入字符串并使用字母频率找到最佳可能的字符串 如果不确定这有多大意义 但让我们提出问题 编写一个执行以下操作的程序 首先 它应该读取一行输入 这是编码消息 由大写字母和空格组成 您
  • 使用 SALib 工具箱对测量数据进行 Python 敏感性分析

    我想了解 如何使用 SALib python 工具箱进行 Sobol 敏感性分析 研究参数和交叉参数影响 从最初的例子我应该这样做 from SALib sample import saltelli from SALib analyze i
  • 如何在 R 中将 `foreach` 和 `%dopar%` 与 `R6` 类一起使用?

    我在尝试使用时遇到了问题 dopar and foreach 与一个R6班级 四处搜索 我只能找到两个与此相关的资源 一个未答复所以问题和一个开放的GitHub问题 on the R6存储库 在一条评论 即 GitHub 问题 中 建议通过
  • Android - 一个选项卡中的多个片段

    我在网上搜索了很多有关在一个操作栏选项卡中包含多个片段的可能性的信息 这个问题最接近我的需求 但代码不起作用 是否有另一种可能性可以在一个选项卡中包含多个片段 这是StartActivity public class StartActivi
  • IIS 7.0 错误 HTTP 错误 500.19 - 内部服务器错误(错误代码 0x80070003)

    我的 Windows 2008 Server 上运行着 IIS7 它非常适合我之前托管的网站和文件夹 现在我尝试托管一个包含两个子文件夹的文件夹 子文件夹包含一些 图片 我已正确设置站点文件夹的 IIS IUSRS 权限 当我尝试浏览时 我
  • 常量值无法转换为 int

    我不明白为什么第 5 行无法编译 而第 4 行却可以 static void Main string args byte b 0 int i int 0xffffff00 b ok int j int 0xffffff00 byte 0 e
  • MVC 中的模型应该如何构建? [关闭]

    Closed 这个问题是基于意见的 目前不接受答案 我刚刚掌握 MVC 框架 经常想知道模型中应该包含多少代码 我倾向于拥有一个具有如下方法的数据访问类 public function CheckUsername connection us