MVC中应该如何构建模型?
我只是掌握了MVC框架,我经常想知道模型中应该有多少代码。 我倾向于有一个类似这样的方法的数据访问类:
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与我们在网页开发中使用的内容有所不同。 这是我写的一个较旧的答案,它简要描述了它们的不同之处。
什么样的模式不是:
该模型不是一个类或任何单个对象。 这是一个非常常见的错误(我也这样做了,尽管原来的答案是在我开始学习时写的),因为大多数框架会使这种误解永久化。
它也不是一个对象关系映射技术(ORM),也不是数据库表的抽象。 任何告诉你的人最有可能试图“出售”另一种全新的ORM或整个框架。
什么是模型:
在适当的MVC适应中,M包含了所有的领域业务逻辑,模型层主要由三种类型的结构组成:
域对象
域对象是纯域信息的逻辑容器; 它通常代表问题域空间中的逻辑实体。 通常被称为业务逻辑。
这将是您定义在发送发票前如何验证数据或计算订单总成本的地方。 与此同时,域对象完全不了解存储 - 无论从何处(SQL数据库,REST API,文本文件等),也不保存或检索。
数据映射器
这些对象只负责存储。 如果将信息存储在数据库中,则这将是SQL所在的位置。 或者,也许您使用XML文件来存储数据,并且您的Data Mappers正在解析XML文件。
服务
您可以将它们视为“更高级别的域对象”,但不是业务逻辑,而是负责域对象和映射器之间的交互。 这些结构最终创建了一个用于与域业务逻辑进行交互的“公共”接口。 您可以避免它们,但是会将一些域逻辑泄漏到控制器中。
在ACL实现问题中有一个与此主题相关的答案 - 它可能很有用。
模型层和MVC三元组的其他部分之间的通信只能通过服务进行。 明确的分离有一些额外的好处:
如何与模型进行交互?
先决条件:观看“全球州和单身人士”讲座和“不要找东西!” 来自Clean Code Talks。
获得服务实例的访问权限
对于View和Controller实例(您可以调用:“UI层”)来访问这些服务,有两种常用方法:
正如你可能会怀疑的那样,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查询,就像你在这里或这里看到的一样(请不要讨厌它们,它们被误导,而不是邪恶)。
向用户显示状态变化。
好的,用户已经登录(或失败)。 怎么办? 所述用户仍然不知道它。 所以你需要实际产生一个回应,这是一个观点的责任。
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
在这种情况下,视图基于模型层的当前状态产生了两种可能的响应之一。 对于不同的用例,您可以根据“当前选择的文章”等内容选择不同的模板进行渲染。
表示层实际上可以非常精细,如下所述:了解PHP中的MVC视图。
但我只是在制作一个REST API!
当然,有些情况下,这是一个矫枉过正的情况。
MVC只是分离关注原则的具体解决方案。 MVC将用户界面与业务逻辑分离开来,并在用户界面中将用户输入和演示分开处理。 这是至关重要的。 虽然人们经常把它形容为“黑社会”,但实际上并不是由三个独立的部分组成。 结构更像这样:
这意味着,当您的表示层的逻辑几乎不存在时,实用的方法是将它们保留为单层。 它也可以大大简化模型层的某些方面。
使用这种方法,登录示例(对于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(EntityIdentity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(MapperIdentity::class);
$mapper->store($identity);
}
创建映射器的方法
要实现持久化的抽象,最灵活的方法是创建自定义数据映射器。
来自:PoEAA书
在实践中,它们被实现用于与特定类别或超类的交互。 假设你的代码中有Customer
和Admin
(都是从User
超类继承的)。 两者可能最终都会有一个单独的匹配映射器,因为它们包含不同的字段。 但是你也将结束共享和常用的操作。 例如:更新“上次在线时间”。 而不是让现有的映射器更复杂,更实用的方法是有一个通用的“用户映射器”,它只更新该时间戳。
一些额外的评论:
数据库表和模型
虽然有时在数据库表,域对象和映射器之间存在直接的1:1:1关系,但在较大的项目中,它可能不如您预期的常见:
单个域对象使用的信息可能会从不同的表映射,而对象本身在数据库中没有持久性。
例如:如果您正在生成月度报告。 这将收集来自不同表格的信息,但数据库中没有神奇的MonthlyReport
表格。
一个Mapper可以影响多个表。
例如:当您存储来自User
对象的数据时,该域对象可能包含其他域对象的集合 - Group
实例。 如果您改变它们并存储User
,Data Mapper必须更新和/或插入多个表中的条目。
来自单个域对象的数据存储在多个表中。
例如:在大型系统(想想:一个中等规模的社交网络)中,将用户身份验证数据和经常访问的数据与较大的内容块分开存储(这很少需要)可能是务实的。 在这种情况下,您可能仍然有一个User
类,但其中包含的信息取决于是否提取了全部细节。
对于每个域对象,可以有多个映射器
例如:您有一个新闻网站,其中包含面向公众和管理软件的共享代码库。 但是,虽然两个界面使用相同的Article
类,但管理需要更多的信息。 在这种情况下,您将有两个单独的映射器:“内部”和“外部”。 每个执行不同的查询,甚至使用不同的数据库(如主站或从站)。
视图不是模板
在MVC中查看实例(如果您不使用模式的MVP变体)负责表示逻辑。 这意味着每个视图通常会玩弄至少几个模板。 它从模型层获取数据,然后根据收到的信息选择一个模板并设置值。
您从中获得的好处之一就是可重用性。 如果您创建了一个ListView
类,那么通过编写良好的代码,您可以让同一个类在文章下面显示用户列表和注释。 因为它们都具有相同的表示逻辑。 你只需切换模板。
您可以使用原生PHP模板或使用某些第三方模板引擎。 也可能有一些第三方库,它们能够完全替代View实例。
那么老版本的答案呢?
唯一的主要变化是,旧版本中称为Model的实际上是一个Service。 其余的“图书馆比喻”保持得很好。
我看到的唯一缺陷是这将是一个非常奇怪的库,因为它会从书中返回信息,但不会让你触及本书,否则抽象将开始“泄漏”。 我可能不得不想出更合适的比喻。
View和Controller实例之间的关系是什么?
MVC结构由两层构成:UI和模型。 UI层的主要结构是视图和控制器。
在处理使用MVC设计模式的网站时,最好的方法是在视图和控制器之间建立1:1的关系。 每个视图都代表您网站中的整个页面,并且它具有专用控制器来处理该特定视图的所有传入请求。
例如,要表示已打开的文章,您应该有ApplicationControllerDocument
和ApplicationViewDocument
。 这将包含UI层的所有主要功能,当涉及到处理文章(当然,您可能有一些XHR组件与文章没有直接关系)。
所有业务逻辑都属于模型,无论是数据库查询,计算,REST调用等。
您可以在模型中拥有数据访问权限,MVC模式不会限制您这样做。 你可以用服务,映射器和其他东西来包装它,但模型的实际定义是处理业务逻辑的一个层,不过也不例外。 它可以是一个类,一个函数或一个带有gazillion对象的完整模块,如果这是你想要的。
有一个单独的对象实际上执行数据库查询,而不是直接在模型中执行它们总是更容易:当单元测试时(由于模型中容易注入模拟数据库依赖项),这将特别方便:
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中,您很少需要捕获/重新抛出异常,因为回溯被保留,特别是在您的示例中。 只需让异常被抛出并在控制器中捕获它。
在Web-“MVC”中,你可以做任何你喜欢的事情。
最初的概念(1)将模型描述为业务逻辑。 它应该代表应用程序状态并强制执行一些数据一致性。 这种方法通常被称为“胖模式”。
大多数PHP框架遵循更浅的方法,其中模型仅仅是数据库接口。 但至少这些模型仍然应该验证传入的数据和关系。
无论哪种方式,如果将SQL资料或数据库调用分隔到另一个层中,则不会太远。 这样您只需关心真实的数据/行为,而不是实际的存储API。 (但是过度使用它是不合理的,如果没有提前设计,你将永远无法用文件存储替换数据库后端。)
链接地址: http://www.djcxy.com/p/60443.html