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 层”)来访问这些服务,有两种通用方法:
- 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用 DI 容器。
- 使用服务工厂作为所有视图和控制器的强制依赖项。
正如您可能怀疑的那样,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 中将用户输入的处理和表示分开。这一点至关重要。虽然人们经常将其描述为“三合会”,但它实际上并不是由三个独立的部分组成。结构更像是这样的:
这意味着,当表示层的逻辑几乎不存在时,务实的方法是将它们保留为单层。它还可以大大简化模型层的某些方面。
使用这种方法,登录示例(对于 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);
}
创建映射器的方法
要实现持久性的抽象,最灵活的方法是创建自定义数据映射器.
From: PoEAA book
在实践中,它们是为了与特定类或超类交互而实现的。假设你有Customer
and Admin
在你的代码中(都继承自User
超类)。两者最终可能都会有一个单独的匹配映射器,因为它们包含不同的字段。但您最终也会得到共享和常用的操作。例如:更新“最后一次在线看到”时间。更实用的方法是拥有一个通用的“用户映射器”,它只更新时间戳,而不是让现有的映射器更加复杂。
一些补充意见:
-
数据库表和模型
虽然有时数据库表之间存在直接的 1:1:1 关系,域对象, and Mapper,在较大的项目中,它可能比您预期的要少见:
-
单个人使用的信息域对象可能是从不同的表映射的,而对象本身在数据库中没有持久性。
Example:如果您要生成月度报告。这将从不同的表收集信息,但没有神奇的MonthlyReport
数据库中的表。
-
单个Mapper可以影响多个表。
Example:当您存储数据时User
对象,这个域对象可以包含其他域对象的集合 -Group
实例。如果您更改它们并存储User
, the 数据映射器必须在多个表中更新和/或插入条目。
-
数据来自单个域对象存储在多个表中。
Example:在大型系统中(例如:中型社交网络),将用户身份验证数据和经常访问的数据与较大的内容块分开存储可能是务实的,但很少需要这样做。在这种情况下,您可能仍然有一个User
类,但它包含的信息取决于是否获取完整的详细信息。
-
对于每一个域对象可以有多个映射器
Example:您有一个新闻网站,该网站具有面向公众的共享代码和管理软件。但是,虽然两个接口使用相同的Article
类,管理层需要在其中填充更多信息。在这种情况下,您将有两个单独的映射器:“内部”和“外部”。每个执行不同的查询,甚至使用不同的数据库(如在主数据库或从数据库中)。
-
视图不是模板
ViewMVC 中的实例(如果您没有使用该模式的 MVP 变体)负责表示逻辑。这意味着每个View通常会同时使用至少几个模板。它从以下位置获取数据模型层然后,根据收到的信息,选择模板并设置值。
您从中获得的好处之一是可重用性。如果您创建一个ListView
类,然后,通过编写良好的代码,您可以让同一个类处理文章下面的用户列表和评论的表示。因为它们都有相同的呈现逻辑。您只需切换模板即可。
您可以使用原生 PHP 模板或者使用一些第三方模板引擎。可能还有一些第三方库,它们可以完全替代View实例。
-
旧版本的答案怎么样?
唯一的重大变化是,所谓的Model在旧版本中,实际上是Service。 “图书馆类比”的其余部分保持得很好。
我看到的唯一缺陷是,这将是一个非常奇怪的库,因为它会返回书中的信息,但不会让你接触书本身,因为否则抽象就会开始“泄漏”。我可能不得不想一个更合适的类比。
-
之间有什么关系View and 控制器实例?
MVC结构由两层组成:ui层和model层。区内主要建筑物UI layer是视图和控制器。
当您处理使用 MVC 设计模式的网站时,最好的方法是在视图和控制器之间建立 1:1 的关系。每个视图代表网站中的整个页面,并且它有一个专用控制器来处理该特定视图的所有传入请求。
例如,要表示一篇打开的文章,您需要\Application\Controller\Document
and \Application\View\Document
。这将包含 UI 层处理文章时的所有主要功能(当然你可能有一些XHR与文章不直接相关的组件).