[英]How should a model be structured in MVC? [closed]
我刚刚掌握了 MVC 框架,我经常想知道 model 中的 go 应该有多少代码。 我倾向于使用具有以下方法的数据访问 class :
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;
}
}
我的模型往往是映射到数据库表的实体 class。
model object 是否应该具有所有数据库映射属性以及上面的代码,或者是否可以将代码分开以使数据库实际工作?
我最终会有四层吗?
免责声明:以下是我如何在基于 PHP 的 web 应用程序的上下文中理解 MVC 样模式的描述。 内容中使用的所有外部链接都是为了解释术语和概念,而不是暗示我自己对该主题的可信度。
我必须澄清的第一件事是: model 是一个层。
第二:经典MVC和我们在web开发中使用的有区别。 这是我写的一个较旧的答案,简要描述了它们的不同之处。
model 不是 class 或任何单个 object。 这是一个非常常见的错误(我也犯了,虽然最初的答案是在我开始学习时写的) ,因为大多数框架都会延续这种误解。
它既不是对象关系映射技术 (ORM),也不是数据库表的抽象。 任何告诉您其他情况的人很可能试图“出售”另一个全新的 ORM 或整个框架。
在适当的 MVC 适配中,M 包含所有领域业务逻辑, Model 层主要由三种类型的结构组成:
域 object 是纯域信息的逻辑容器; 它通常代表问题域空间中的一个逻辑实体。 通常称为业务逻辑。
您可以在此处定义如何在发送发票之前验证数据,或计算订单的总成本。 同时,域对象完全不知道存储——无论是从哪里(SQL 数据库、REST API、文本文件等),即使它们被保存或检索。
这些对象只负责存储。 如果您将信息存储在数据库中,这将是 SQL 所在的位置。 或者,也许您使用 XML 文件来存储数据,并且您的数据映射器正在解析 XML 文件。
您可以将它们视为“更高级别的域对象”,但服务不是业务逻辑,而是负责域对象和映射器之间的交互。 这些结构最终创建了一个用于与域业务逻辑交互的“公共”接口。 您可以避免它们,但代价是会将一些域逻辑泄漏到Controllers中。
在ACL implementation question 中有一个与此主题相关的答案 - 它可能很有用。
model 层和 MVC 三元组的其他部分之间的通信只能通过Services进行。 清晰的分离还有一些额外的好处:
先决条件:观看讲座“全球 State 和单身人士”和“不要找东西!” 来自清洁代码会谈。
对于View和Controller实例(您可以称之为:“UI 层”)访问这些服务,有两种通用方法:
您可能会怀疑,DI 容器是一个更优雅的解决方案(虽然对于初学者来说不是最简单的)。 我建议考虑使用此功能的两个库是 Syfmony 的独立DependencyInjection 组件或Auryn 。
使用工厂和 DI 容器的解决方案都可以让您共享要在所选 controller 之间共享的各种服务器的实例,并查看给定的请求-响应周期。
现在您可以访问控制器中的 model 层,您需要开始实际使用它们:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
您的控制器有一个非常明确的任务:接受用户输入,并根据此输入更改当前 state 的业务逻辑。 在此示例中,更改的状态是“匿名用户”和“登录用户”。
Controller 不负责验证用户的输入,因为这是业务规则的一部分,并且 controller 绝对不会调用 SQL 查询(不正确的查询,不讨厌他们),他们是错误的,就像你会看到的那样,不要在这里看到。
好的,用户已登录(或失败)。 怎么办? 所述用户仍然没有意识到这一点。 因此,您需要实际产生响应,这是视图的责任。
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
在这种情况下,视图根据 model 层的当前 state 产生了两种可能的响应之一。 对于不同的用例,您可以让视图根据“当前选择的文章”之类的内容选择不同的模板进行渲染。
表示层实际上可以变得非常复杂,如下所述:了解 PHP 中的 MVC 视图。
当然,在某些情况下,这是矫枉过正的。
MVC 只是关注点分离原则的具体解决方案。 MVC 将用户界面与业务逻辑分离,在 UI 中将用户输入的处理和表示分离。 这是至关重要的。 虽然人们经常将其描述为“三合会”,但它实际上并不是由三个独立的部分组成。 结构更像这样:
这意味着,当您的表示层的逻辑几乎不存在时,实用的方法是将它们保持为单层。 它还可以大大简化 model 层的某些方面。
使用这种方法,登录示例(对于 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);
}
虽然这是不可持续的,但当您有复杂的逻辑来呈现响应主体时,这种简化对于更琐碎的场景非常有用。 但请注意,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。
由于没有单个“模型”class(如上所述),因此您确实没有“构建模型”。 相反,您从制作服务开始,它能够执行某些方法。 然后实现Domain Objects和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();
}
如您所见,在这个抽象级别上,没有迹象表明数据是从哪里获取的。 它可能是一个数据库,但也可能只是一个用于测试目的的模拟 object。 即使是实际使用的数据映射器,也隐藏在该服务的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
超类)。 两者可能最终都有一个单独的匹配映射器,因为它们包含不同的字段。 但是您最终也会得到共享和常用的操作。 例如:更新“最后一次在线”时间。 而不是让现有的映射器更加复杂,更实用的方法是拥有一个通用的“用户映射器”,它只更新那个时间戳。
数据库表和 model
虽然有时数据库表、域 Object和Mapper之间存在直接的 1:1:1 关系,但在较大的项目中,它可能不像您预期的那样常见:
单个域 Object使用的信息可能来自不同的表,而 object 本身在数据库中没有持久性。
示例:如果您正在生成月度报告。 这将从不同的表中收集信息,但数据库中没有神奇的MonthlyReport
表。
一个Mapper可以影响多个表。
示例:当您存储来自User
object 的数据时,此域 Object可能包含其他域对象的集合 - Group
实例。 如果您更改它们并存储User
,则数据映射器将不得不更新和/或在多个表中插入条目。
来自单个域 Object的数据存储在多个表中。
示例:在大型系统中(想想:一个中型社交网络),将用户身份验证数据和经常访问的数据与大块内容分开存储可能是实用的,这很少需要。 在这种情况下,您可能仍然只有一个User
class,但它包含的信息取决于是否获取了完整的详细信息。
对于每个域 Object可以有多个映射器
示例:您有一个新闻站点,其中包含面向公众和管理软件的共享代码。 但是,虽然两个界面都使用相同的Article
class,但管理需要在其中填充更多信息。 在这种情况下,您将有两个独立的映射器:“内部”和“外部”。 每个执行不同的查询,甚至使用不同的数据库(如在 master 或 slave 中)。
视图不是模板
MVC 中的视图实例(如果您不使用模式的 MVP 变体)负责表示逻辑。 这意味着每个视图通常会处理至少几个模板。 它从Model 层获取数据,然后根据接收到的信息选择模板并设置值。
您从中获得的好处之一是可重用性。 如果您创建一个ListView
class,那么,通过编写良好的代码,您可以让相同的 class 处理文章下方的用户列表和评论的呈现。 因为它们都有相同的表示逻辑。 您只需切换模板。
您可以使用本机 PHP 模板或使用某些第三方模板引擎。 也可能有一些第三方库,它们能够完全替换View实例。
旧版本的答案呢?
唯一的主要变化是,旧版本中所谓的Model实际上是一个Service 。 “类比库”的 rest 保持得很好。
我看到的唯一缺陷是这将是一个非常奇怪的图书馆,因为它会从书中返回信息,但不会让你触摸书本身,否则抽象会开始“泄漏”。 我可能不得不想一个更贴切的比喻。
View和Controller实例之间有什么关系?
MVC结构由两层组成:ui和model。 UI层的主要结构是views和controller。
当您处理使用 MVC 设计模式的网站时,最好的方法是在视图和控制器之间建立 1:1 的关系。 每个视图代表您网站中的整个页面,它有一个专用的 controller 来处理该特定视图的所有传入请求。
例如,要表示打开的文章,您将拥有\Application\Controller\Document
和\Application\View\Document
。 这将包含 UI 层的所有主要功能,当涉及到处理文章时(当然,您可能有一些与文章没有直接关系的XHR组件) 。
业务逻辑的一切都属于一个 model,无论是数据库查询、计算、REST 调用等。
您可以在 model 本身中进行数据访问,MVC 模式不会限制您这样做。 您可以用服务、映射器等对其进行修饰,但 model 的实际定义是处理业务逻辑的层,仅此而已。 它可以是 class、function,也可以是包含大量对象的完整模块(如果这是您想要的)。
拥有一个单独的 object 总是更容易实际执行数据库查询,而不是直接在 model 中执行它们:这在单元测试时特别有用(因为在模型中注入模拟数据库依赖项很容易):
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 中,您很少需要捕获/重新抛出异常,因为保留了回溯,尤其是在像您的示例这样的情况下。 只需抛出异常并在 controller 中捕获它。
在 Web-“MVC”中,您可以随心所欲。
原始概念(1)将 model 描述为业务逻辑。 它应该代表应用程序 state 并强制执行一些数据一致性。 这种方法通常被描述为“胖模型”。
大多数 PHP 框架遵循更浅的方法,其中 model 只是一个数据库接口。 但至少这些模型仍应验证传入的数据和关系。
无论哪种方式,如果您将 SQL 内容或数据库调用分离到另一层,您就不会太远了。 这样你只需要关心真实的数据/行为,而不是实际的存储 API。 (但是,过度使用它是不合理的。如果没有提前设计,您将永远无法用文件存储替换数据库后端。)
更常见的是,大多数应用程序都有数据、显示和处理部分,我们只是将所有这些都放在字母M
、 V
和C
中。
模型( M
) ->具有包含应用程序 state 的属性,并且它不知道有关V
和C
的任何事情。
View( V
) --> 有应用程序的显示格式,只知道如何消化 model ,不关心C
。
控制器( C
) ---->具有应用程序的处理部分,充当 M 和 V 之间的接线,它依赖于M
, V
与M
和V
不同。
总而言之,每个人之间存在关注点分离。 将来可以很容易地添加任何更改或增强功能。
就我而言,我有一个数据库 class 来处理所有直接的数据库交互,例如查询、获取等。 因此,如果我必须将我的数据库从MySQL更改为PostgreSQL不会有任何问题。 因此,添加额外的层可能很有用。
每个表可以有自己的 class 并有其特定的方法,但要实际获取数据,它让数据库 class 处理它:
Database.php
class Database {
private static $connection;
private static $current_query;
...
public static function query($sql) {
if (!self::$connection){
self::open_connection();
}
self::$current_query = $sql;
$result = mysql_query($sql,self::$connection);
if (!$result){
self::close_connection();
// throw custom error
// The query failed for some reason. here is query :: self::$current_query
$error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
$error->handleError();
}
return $result;
}
....
public static function find_by_sql($sql){
if (!is_string($sql))
return false;
$result_set = self::query($sql);
$obj_arr = array();
while ($row = self::fetch_array($result_set))
{
$obj_arr[] = self::instantiate($row);
}
return $obj_arr;
}
}
表 object classL
class DomainPeer extends Database {
public static function getDomainInfoList() {
$sql = 'SELECT ';
$sql .='d.`id`,';
$sql .='d.`name`,';
$sql .='d.`shortName`,';
$sql .='d.`created_at`,';
$sql .='d.`updated_at`,';
$sql .='count(q.id) as queries ';
$sql .='FROM `domains` d ';
$sql .='LEFT JOIN queries q on q.domainId = d.id ';
$sql .='GROUP BY d.id';
return self::find_by_sql($sql);
}
....
}
我希望这个例子可以帮助你创建一个好的结构。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.