免责声明:以下是我如何在基于PHP的Web应用程序上下文中理解类似MVC的模式的描述。内容中使用的所有外部链接都是为了解释术语和概念,而不是暗示我自己在这个问题上的可信度。
我必须澄清的第一件事是:模型是一个层。
第二:经典的MVC和我们在Web开发中使用的MVC是有区别的。这是我写的一个较旧的答案,它简要描述了它们的不同之处。
模型不是什么:
模型不是类或任何单个对象。这是一个非常常见的错误(我也犯了,尽管最初的答案是在我开始学习其他错误时写的),因为大多数框架都会延续这种误解。
它既不是对象关系映射技术(ORM),也不是数据库表的抽象。任何告诉你相反情况的人都很可能试图“出售”另一个全新的ORM或整个框架。
什么是模型:
在适当的MVC适应中,M包含所有领域业务逻辑,模型层主要由三种类型的结构组成:
-
域对象
域对象是纯域信息的逻辑容器;它通常表示问题域空间中的逻辑实体。通常称为业务逻辑。
在这里,您可以定义如何在发送发票之前验证数据,或计算订单的总成本。同时,域对象完全不知道存储 - 既不是从哪里(SQL数据库,REST API,文本文件等)也不是,即使它们被保存或检索。
-
数据映射器
这些对象仅负责存储。如果将信息存储在数据库中,则这将是 SQL 所在的位置。或者,您可能使用 XML 文件来存储数据,并且数据映射器正在从 XML 文件解析和解析到 XML 文件。
-
服务业
您可以将它们视为“更高级别的域对象”,但服务负责域对象和映射器之间的交互,而不是业务逻辑。这些结构最终创建了一个用于与域业务逻辑交互的“公共”接口。您可以避免它们,但会受到将某些域逻辑泄漏到控制器中的惩罚。
在 ACL 实现问题中有一个与此主题的相关答案 - 它可能很有用。
模型层与 MVC 三元组的其他部分之间的通信应仅通过服务进行。清晰的分离还有一些额外的好处:
- 它有助于执行单一责任原则(SRP)
- 提供额外的“回旋余地”,以防逻辑发生变化
- 使控制器尽可能简单
- 给出了一个清晰的蓝图,如果你曾经需要一个外部API
如何与模型交互?
先决条件:观看Clean Code Talks中的讲座“Global State and Singletons”和“Don't Look For Things!”
获取对服务实例的访问权限
对于要访问这些服务的视图和控制器实例(可以称为“UI 层”),有两种常规方法:
- 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用 DI 容器。
- 使用服务的工厂作为所有视图和控制器的强制依赖项。
正如您可能怀疑的那样,DI容器是一种更优雅的解决方案(虽然对于初学者来说不是最简单的)。我建议考虑使用此功能的两个库是Syfmony的独立依赖注入组件或Auryn。
使用工厂和 DI 容器的解决方案还允许您共享各种服务器的实例,以便在给定的请求-响应周期内在所选控制器和视图之间共享。
模型状态的改变
现在,您可以访问控制器中的模型层,您需要开始实际使用它们:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
控制器有一个非常明确的任务:获取用户输入,并根据此输入更改业务逻辑的当前状态。在此示例中,在“匿名用户”和“登录用户”之间更改的状态。
控制器不负责验证用户的输入,因为这是业务规则的一部分,控制器绝对不会调用SQL查询,就像你在这里或这里看到的那样(请不要讨厌它们,它们是误导的,而不是邪恶的)。
向用户显示状态更改。
好的,用户已登录(或失败)。现在怎么办?所述用户仍然不知道它。因此,您需要实际产生响应,这是视图的责任。
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
在本例中,视图根据模型层的当前状态生成了两种可能的响应之一。对于不同的用例,您将让视图根据“当前选择的文章”之类的内容选择要呈现的不同模板。
表示层实际上可以变得非常复杂,如下所述:了解PHP中的MVC视图。
但我只是在做一个REST API!
当然,在某些情况下,这是一个过度的。
MVC只是关注点分离原则的具体解决方案。MVC将用户界面与业务逻辑分开,在UI中它分离了用户输入和表示的处理。这一点至关重要。虽然人们经常将其描述为“三合会”,但它实际上并不是由三个独立的部分组成的。结构更像这样:
这意味着,当您的表示层的逻辑几乎不存在时,实用的方法是将它们保留为单个层。它还可以大大简化模型层的某些方面。
使用这种方法,登录示例(对于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);
}
虽然这是不可持续的,但当您具有用于呈现响应正文的复杂逻辑时,这种简化对于更琐碎的方案非常有用。但请注意,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。
如何构建模型?
由于没有一个“模型”类(如上所述),因此您实际上不会“构建模型”。相反,您从创建能够执行某些方法的服务开始。然后实现域对象和映射器。
服务方法的示例:
在上面的两种方法中,都有用于标识服务的登录方法。它实际上会是什么样子。我正在使用我编写的库中相同功能的略微修改版本。因为我懒惰:
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);
}
创建映射器的方法
若要实现持久性的抽象,最灵活的方法是创建自定义数据映射器。
寄件人: PoEAA 书籍
在实践中,它们是为与特定类或超类的交互而实现的。假设你有和在你的代码中(两者都继承自超类)。两者最终都可能具有单独的匹配映射器,因为它们包含不同的字段。但是,您最终也会使用共享和常用的操作。例如:更新“上次在线查看”时间。与其使现有的映射器更加复杂,更务实的方法是使用一个通用的“用户映射器”,它只更新该时间戳。Customer
Admin
User
一些额外的评论:
-
数据库表和模型
虽然有时数据库表、域对象和映射器之间存在直接的 1:1:1 关系,但在大型项目中,它可能不如您预期的常见:
-
单个域对象使用的信息可能从不同的表中映射,而对象本身在数据库中没有持久性。
示例:如果您正在生成月度报告。这将从不同的表中收集信息,但数据库中没有神奇的表。MonthlyReport
-
单个映射器可以影响多个表。
示例:当您存储对象中的数据时,此域对象可能包含其他域对象的集合 - 实例。如果更改它们并存储 ,则数据映射器必须更新和/或在多个表中插入条目。User
Group
User
-
来自单个域对象的数据存储在多个表中。
示例:在大型系统中(例如:中型社交网络),将用户身份验证数据和经常访问的数据与较大的内容块分开存储可能是实用的,这很少需要。在这种情况下,您可能仍然有一个类,但它包含的信息将取决于是否获取了完整的详细信息。User
-
对于每个域对象,可以有多个映射器
示例:您有一个新闻网站,其中包含面向公众和管理软件的共享代码。但是,虽然两个接口都使用相同的类,但管理需要在其中填充更多信息。在这种情况下,您将有两个单独的映射器:“内部”和“外部”。每个执行不同的查询,甚至使用不同的数据库(如主数据库或从数据库)。Article
-
视图不是模板
MVC 中的视图实例(如果未使用模式的 MVP 变体)负责表示逻辑。这意味着每个视图通常会处理至少几个模板。它从模型层获取数据,然后根据收到的信息选择模板并设置值。
您从中获得的好处之一是可重用性。如果你创建了一个类,那么,使用写得很好的代码,你可以让同一个类处理用户列表和文章下面的注释的呈现。因为它们都具有相同的表示逻辑。您只需切换模板即可。ListView
您可以使用本机 PHP 模板,也可以使用某些第三方模板引擎。可能还有一些第三方库,它们能够完全替换 View 实例。
-
那么旧版本的答案呢?
唯一的主要变化是,在旧版本中所谓的模型实际上是一个服务。“库类比”的其余部分保持得很好。
我看到的唯一缺陷是,这将是一个非常奇怪的图书馆,因为它会从书中返回你的信息,但不让你触摸书本身,因为否则抽象会开始“泄漏”。我可能不得不想出一个更合适的类比。
-
视图实例和控制器实例之间的关系是什么?
MVC 结构由两层组成:ui 和模型。UI层中的主要结构是视图和控制器。
当您处理使用 MVC 设计模式的网站时,最好的方法是在视图和控制器之间建立 1:1 的关系。每个视图代表您网站中的整个页面,它有一个专用的控制器来处理该特定视图的所有传入请求。
例如,要表示打开的文章,可以使用 和 。这将包含UI层在处理文章时的所有主要功能(当然,您可能有一些与文章没有直接关系的XHR组件)。\Application\Controller\Document
\Application\View\Document