MVC中应该如何构build模型?

我只是掌握了MVC框架,我经常想知道模型中应该有多less代码。 我倾向于有一个像这样的方法的数据访问类:

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; } } 

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

模型对象应该具有所有数据库映射的属性以及上面的代码,还是可以将代码分离出来,实际上数据库可以工作吗?

我最终会有四层?

免责声明:以下是我如何理解基于PHP的Web应用程序环境中类MVC模式的描述。 内容中使用的所有外部链接都是为了解释术语和概念,而不是暗示我自己对这个主题的可信度。

我必须清楚的第一件事是: 模型是一个层

其次: 经典的MVC和我们在网页开发中使用的有所不同。 这是我写的一个较老的答案,它简要地描述了它们的不同之处。

什么样的模式不是:

该模型不是一个类或任何单个对象。 这是一个非常常见的错误(我也这么做了,尽pipe原来的答案是在我开始学习的时候写的) ,因为大多数框架都会使这种误解永久化。

它也不是一个对象关系映射技术(ORM),也不是数据库表的抽象。 任何告诉你的人最有可能试图“出售”另一个全新的ORM或整个框架。

什么样的模式是:

在适当的MVC适应中,M包含了所有的领域业务逻辑, 模型层 主要由三种结构types组成:

  • 域对象

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

    这将是您定义在发送发票之前如何validation数据或计算订单总成本的地方。 与此同时, 域对象完全不了解存储 – 无论从何处 (SQL数据库,REST API,文本文件等),也不保存或检索。

  • 数据映射器

    这些对象只负责存储。 如果将信息存储在数据库中,则这将是SQL所在的位置。 或者,也许你使用XML文件来存储数据,你的数据映射器是从XML文件parsing。

  • 服务

    您可以将它们视为“更高级的域对象”,而不是业务逻辑, 服务负责域对象映射器之间的交互。 这些结构最终创build了一个用于与域业务逻辑交互的“公共”接口。 你可以避免它们,但是将一些领域逻辑泄漏到控制器的惩罚中。

    ACL实现问题中有一个与此主题相关的答案 – 它可能是有用的。

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

  • 它有助于执行单一责任原则 (SRP)
  • 在逻辑改变的情况下提供额外的“摆动室”
  • 尽可能简化控制器
  • 给出了一个清晰的蓝图,如果你需要一个外部的API

如何与模型进行交互?

先决条件:观看“全球状态和单身人士”和“不要找东西! 来自Clean Code Talks。

获得服务实例的访问权限

对于ViewController实例(你可以称之为“UI层”)来访问这些服务,有两种一般的方法:

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

正如你可能会怀疑的那样,DI容器是一个更加优雅的解决scheme(虽然对于初学者来说不是最简单的)。 这两个库,我build议考虑这个function将是Syfmony的独立DependencyInjection组件或Auryn 。

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

改变模型的状态

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

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

您的控制器有一个非常明确的任务:接受用户input,并根据此input更改业务逻辑的当前状态。 在这个例子中,改变的状态是“匿名用户”和“login用户”。

控制器不负责validation用户的input,因为这是业务规则的一部分,控制器绝对不会调用SQL查询,就像你会在这里或这里看到的(请不要讨厌它们,它们被误导,而不是邪恶)。

向用户显示状态变化。

好的,用户已经login(或失败)。 怎么办? 所述用户仍然不知道它。 所以你需要真正产生一个回应,这是一个观点的责任。

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

在这种情况下,基于模型层的当前状态,视图产生了两种可能的响应之一。 对于不同的用例,您可以根据“当前所选文章”等内容,select不同的模板进行渲染。

performance层实际上可以相当复杂,如下所述: 了解PHP中的MVC视图 。

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

当然,有些情况下,这是一个矫枉过正的情况。

MVC只是“ 分离关注”原则的具体解决scheme。 MVC将用户界面与业务逻辑分离开来,并在用户界面中将用户input和演示处理分开。 这是至关重要的。 虽然人们经常把它形容为“黑社会”,但实际上并不是由三个独立的部分组成的。 结构更像这样:

MVC分离

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

使用这种方法,login示例(对于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); } 

虽然这是不可持续的,但是当渲染响应主体的逻辑复杂化时,这种简化对于更微不足道的场景非常有用。 但是要注意的是 ,当试图在具有复杂表示逻辑的大型代码库中使用时,这种方法将变成一场噩梦。

如何build立模型?

既然没有一个“模型”类(如上所述),你真的不“build立模型”。 相反,您从制作服务开始,可以执行某些方法。 然后实现域对象映射器

服务方法的一个例子:

在上述两种方法中都有这种识别服务的login方法。 它实际上是什么样子。 我正在使用库中相同function的稍微修改过的版本,因为我很懒:

 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(); } 

正如你所看到的,在这个抽象层次上,没有任何迹象表明数据是从哪里获取的。 它可能是一个数据库,但它也可能只是一个模拟对象的testing目的。 即使是实际使用的数据映射器,也隐藏在这个服务的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); } 

创build映射器的方法

为了实现持久化的抽象,最灵活的方法是创build自定义数据映射器 。

映射器图

来自: PoEAA书

在实践中,它们被实现用于与特定类别或超类的交互。 假设你的代码中有CustomerAdmin (都是从User超类inheritance的)。 两者都可能最终会有一个单独的匹配映射器,因为它们包含不同的字段。 但是,你也将最终得到共享和常用的操作。 例如:更新“上次在线”时间。 而不是使现有的映射器更复杂,更实用的方法是有一个普通的“用户映射器”,它只更新该时间戳。

一些额外的评论:

  1. 数据库表和模型

    虽然有时在数据库表, 域对象映射器之间存在直接的1:1:1关系,但是在较大的项目中,它可能不如您预期的常见:

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

      例如:如果您正在生成月度报告。 这将收集来自不同表格的信息,但数据库中没有神奇的MonthlyReport表格。

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

      示例:当您存储User对象的数据时,该域对象可能包含其他域对象( Group实例)的集合。 如果您修改它们并存储User ,则数据映射器将不得不更新和/或插入多个表中的条目。

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

      例如:在大型系统中(想法:一个中等规模的社交networking),将用户身份validation数据和经常访问的数据与较大的内容块分开存储(这很less需要)可能是实用的。 在这种情况下,您可能仍然有一个User类,但其中包含的信息取决于是否提取了全部细节。

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

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

  2. 一个视图不是一个模板

    在MVC中查看实例(如果您没有使用模式的MVP变体)负责表示逻辑。 这意味着每个视图通常会玩弄至less几个模板。 它从模型层获取数据,然后根据收到的信息select一个模板并设置值。

    您从中获得的好处之一是可重用性。 如果您创build了一个ListView类,那么使用精心编写的代码,您可以使用同一个类来处理文章下面的用户列表和注释。 因为他们都有相同的表示逻辑。 你只需切换模板。

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

  3. 老版本的答案呢?

    唯一的主要变化是,旧版本中所谓的Model ,实际上是一个Service 。 其余的“图书馆比喻”保持不错。

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

  4. ViewController实例之间有什么关系?

    MVC结构由两层构成:UI和模型。 UI层的主要结构是视图和控制器。

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

    例如,要表示一个打开的文章,您将有\Application\Controller\Document\Application\View\Document 。 这将包含UI层的所有主要function,当涉及到处理文章(当然,您可能有一些XHR组件与文章没有直接关系)

所有业务逻辑都属于模型,无论是数据库查询,计算,REST调用等等。

你可以在模型本身有数据访问,MVC模式不会限制你这样做。 你可以用服务,映射器和其他的东西来包装它,但是模型的实际定义是一个处理业务逻辑的层,没有什么比这更重要。 它可以是一个类,一个函数,或一个与gazillion对象,如果这是你想要的完整模块。

有一个单独的对象实际上执行数据库查询,而不是直接在模型中执行它总是比较容易的:当unit testing(因为在你的模型中注入一个模拟数据库依赖的简易性),这将特别方便:

 class Database { protected $_conn; public function __construct($connection) { $this->_conn = $connection; } public function ExecuteObject($sql, $data) { // stuff } } abstract class Model { protected $_db; public function __construct(Database $db) { $this->_db = $db; } } class User extends Model { public function CheckUsername($username) { // ... $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ..."; return $this->_db->ExecuteObject($sql, $data); } } $db = new Database($conn); $model = new User($db); $model->CheckUsername('foo'); 

另外,在PHP中,你很less需要捕获/重新抛出exception,因为回溯被保留,特别是在你的例子中。 只要让exception被抛出,并将其捕获到控制器中。

在Web-“MVC”中,你可以做任何你喜欢的事情。

最初的概念(1)将模型描述为业务逻辑。 它应该代表应用程序状态并强制执行一些数据一致性。 这种方法通常被描述为“胖模式”。

大多数PHP框架遵循更浅的方法,其中模型只是一个数据库接口。 但至less这些模型仍然应该validation传入的数据和关系。

无论哪种方式,如果您将SQL资料或数据库调用分隔到另一个层,则不会太远。 这样你只需要关心真实的数据/行为,而不是实际的存储API。 (但是这样做是不合理的,如果没有提前devise,你将永远无法用文件存储replace数据库后端。)

更多的时候,大部分的应用程序都会有数据,显示和处理的部分,我们只是把所有这些都放在字母MVC

模型( M – >具有持有应用程序状态的属性,它不知道关于VC任何事情。

查看( V – >具有显示应用程序的格式,只知道如何对其进行摘要build模,不关心C

控制器( C —->处理部分应用程序,作为M和V之间的接线,并且不依赖于M和V,而依赖于MV

每个人之间总是有分离的关系。 将来任何更改或增强都可以很容易地添加。

在我的情况下,我有一个数据库类,处理所有的直接数据库交互,如查询,抓取,等等。 所以,如果我不得不把我的数据库从MySQL更改为PostgreSQL,那么不会有任何问题。 所以添加额外的层可能是有用的。

每个表可以有自己的类,并有其具体的方法,但实际上获得的数据,它让数据库类来处理它:

文件Database.php

 class Database { private static $connection; private static $current_query; ... public static function query($sql) { if (!self::$connection){ self::open_connection(); } self::$current_query = $sql; $result = mysql_query($sql,self::$connection); if (!$result){ self::close_connection(); // throw custom error // The query failed for some reason. here is query :: self::$current_query $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n"); $error->handleError(); } return $result; } .... public static function find_by_sql($sql){ if (!is_string($sql)) return false; $result_set = self::query($sql); $obj_arr = array(); while ($row = self::fetch_array($result_set)) { $obj_arr[] = self::instantiate($row); } return $obj_arr; } } 

表对象classL

 class DomainPeer extends Database { public static function getDomainInfoList() { $sql = 'SELECT '; $sql .='d.`id`,'; $sql .='d.`name`,'; $sql .='d.`shortName`,'; $sql .='d.`created_at`,'; $sql .='d.`updated_at`,'; $sql .='count(q.id) as queries '; $sql .='FROM `domains` d '; $sql .='LEFT JOIN queries q on q.domainId = d.id '; $sql .='GROUP BY d.id'; return self::find_by_sql($sql); } .... } 

我希望这个例子能帮助你创build一个好的结构。