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