免责声明:以下是我在基于php的web应用程序上下文中如何理解类似mvc的模式的描述。所有在内容中使用的外部链接都是为了解释术语和概念,而不是暗示我自己在这个主题上的可信度。
我必须弄清楚的第一件事是:模型是一个层。
第二:经典的MVC和我们在web开发中使用的MVC是有区别的。这是我以前写过的一个答案,简要地描述了它们的不同之处。
模型不是什么:
模型不是一个类或任何单个对象。这是一个非常常见的错误(我也犯过,尽管最初的答案是在我开始学习其他知识时写的),因为大多数框架都延续了这种误解。
它既不是对象关系映射技术(ORM),也不是数据库表的抽象。任何不这么说的人都很可能是在试图“销售”另一个全新的ORM或整个框架。
什么是模型:
在适当的MVC改编中,M包含了所有的领域业务逻辑,模型层主要由三种类型的结构组成:
Domain Objects
A domain object is a logical container of purely domain information; it usually represents a logical entity in the problem domain space. Commonly referred to as business logic.
This would be where you define how to validate data before sending an invoice, or to compute the total cost of an order. At the same time, Domain Objects are completely unaware of storage - neither from where (SQL database, REST API, text file, etc.) nor even if they get saved or retrieved.
Data Mappers
These objects are only responsible for the storage. If you store information in a database, this would be where the SQL lives. Or maybe you use an XML file to store data, and your Data Mappers are parsing from and to XML files.
Services
You can think of them as "higher level Domain Objects", but instead of business logic, Services are responsible for interaction between Domain Objects and Mappers. These structures end up creating a "public" interface for interacting with the domain business logic. You can avoid them, but at the penalty of leaking some domain logic into Controllers.
There is a related answer to this subject in the ACL implementation question - it might be useful.
模型层和MVC三位一体的其他部分之间的通信应该只通过服务进行。这种明确的分离还有一些额外的好处:
它有助于执行单一责任原则(SRP)
在逻辑发生变化时提供额外的“摆动空间”
保持控制器尽可能简单
如果你需要一个外部API,它会给出一个清晰的蓝图
如何与模型交互?
先决条件:观看Clean Code Talks的讲座“全局状态和单例”和“不要寻找东西!”
获得对服务实例的访问权
对于视图和控制器实例(你可以称之为:“UI层”)都可以访问这些服务,有两种一般的方法:
您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用DI容器。
将服务的工厂用作所有视图和控制器的强制依赖项。
正如您可能怀疑的那样,DI容器是一个更优雅的解决方案(但对于初学者来说不是最简单的)。我建议考虑使用Syfmony的独立DependencyInjection组件或Auryn来实现这个功能。
使用工厂和DI容器的这两种解决方案都允许您在给定的请求-响应周期中,在选定的控制器和视图之间共享各种服务器的实例。
模型状态的改变
现在你可以访问控制器中的模型层,你需要开始实际使用它们:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
您的控制器有一个非常明确的任务:获取用户输入,并根据此输入更改业务逻辑的当前状态。在本例中,在“匿名用户”和“登录用户”之间更改的状态。
控制器不负责验证用户的输入,因为这是业务规则的一部分,控制器肯定不会调用SQL查询,就像你在这里看到的那样(请不要讨厌它们,它们是被误导的,不是邪恶的)。
显示用户状态变化。
Ok,用户已登录(或失败)。现在怎么办呢?该用户仍然不知道它。你需要实际产生一个响应,这是视图的责任。
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);
}
虽然这是不可持续的,但当您有复杂的逻辑来呈现响应体时,这种简化对于更琐碎的场景非常有用。但是请注意,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。
如何构建模型?
因为没有一个单独的“Model”类(如上所述),所以实际上不需要“构建模型”。相反,您可以从创建能够执行某些方法的服务开始。然后实现域对象和映射器。
一个服务方法的例子:
在上述两种方法中,都有这种用于标识服务的登录方法。它实际上会是什么样子。我使用的是从一个库相同的功能略有修改的版本,我写..因为我很懒:
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 function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
创建映射器的方法
要实现持久性的抽象,最灵活的方法是创建自定义数据映射器。
选自:poea书
In practice they are implemented for interaction with specific classes or superclasses. Lets say you have Customer and Admin in your code (both inheriting from a User superclass). Both would probably end up having a separate matching mapper, since they contain different fields. But you will also end up with shared and commonly used operations. For example: updating the "last seen online" time. And instead of making the existing mappers more convoluted, the more pragmatic approach is to have a general "User Mapper", which only update that timestamp.
其他一些意见:
Database tables and model
While sometimes there is a direct 1:1:1 relationship between a database table, Domain Object, and Mapper, in larger projects it might be less common than you expect:
Information used by a single Domain Object might be mapped from different tables, while the object itself has no persistence in the database.
Example: if you are generating a monthly report. This would collect information from different of tables, but there is no magical MonthlyReport table in the database.
A single Mapper can affect multiple tables.
Example: when you are storing data from the User object, this Domain Object could contain collection of other domain objects - Group instances. If you alter them and store the User, the Data Mapper will have to update and/or insert entries in multiple tables.
Data from a single Domain Object is stored in more than one table.
Example: in large systems (think: a medium-sized social network), it might be pragmatic to store user authentication data and often-accessed data separately from larger chunks of content, which is rarely required. In that case you might still have a single User class, but the information it contains would depend of whether full details were fetched.
For every Domain Object there can be more than one mapper
Example: you have a news site with a shared codebased for both public-facing and the management software. But, while both interfaces use the same Article class, the management needs a lot more info populated in it. In this case you would have two separate mappers: "internal" and "external". Each performing different queries, or even use different databases (as in master or slave).
A view is not a template
View instances in MVC (if you are not using the MVP variation of the pattern) are responsible for the presentational logic. This means that each View will usually juggle at least a few templates. It acquires data from the Model Layer and then, based on the received information, chooses a template and sets values.
One of the benefits you gain from this is re-usability. If you create a ListView class, then, with well-written code, you can have the same class handing the presentation of user-list and comments below an article. Because they both have the same presentation logic. You just switch templates.
You can use either native PHP templates or use some third-party templating engine. There also might be some third-party libraries, which are able to fully replace View instances.
What about the old version of the answer?
The only major change is that, what is called Model in the old version, is actually a Service. The rest of the "library analogy" keeps up pretty well.
The only flaw that I see is that this would be a really strange library, because it would return you information from the book, but not let you touch the book itself, because otherwise the abstraction would start to "leak". I might have to think of a more fitting analogy.
What is the relationship between View and Controller instances?
The MVC structure is composed of two layers: ui and model. The main structures in the UI layer are views and controller.
When you are dealing with websites that use MVC design pattern, the best way is to have 1:1 relation between views and controllers. Each view represents a whole page in your website and it has a dedicated controller to handle all the incoming requests for that particular view.
For example, to represent an opened article, you would have \Application\Controller\Document and \Application\View\Document. This would contain all the main functionality for UI layer, when it comes to dealing with articles (of course you might have some XHR components that are not directly related to articles).