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;
}
}

我的模型往往是映射到数据库表的实体类。

模型对象是否应该像上面的代码一样具有所有的数据库映射属性,或者是否可以将实际执行数据库工作的代码分离出来?

我最终会有四层吗?

196339 次浏览

在Web-“MVC”中,你可以做任何你想做的事情。

最初的概念1549970 < a href = " https://stackoverflow.com/questions/1549857/simple-php-mvc-framework/1549970 " > (1) < / >将模型描述为业务逻辑。它应该表示应用程序状态并强制数据一致性。这种方式通常被称为“肥胖模式”。

大多数PHP框架遵循较浅的方法,其中模型只是一个数据库接口。但至少这些模型仍然应该验证传入的数据和关系。

无论哪种方式,如果您将SQL内容或数据库调用分离到另一层,您都不会离目标太远。这样你只需要关心真实的数据/行为,而不需要关心实际的存储API。(然而,做得太过分是不合理的。例如,如果没有提前设计,你将永远无法用文件存储替换数据库后端。)

所有属于业务逻辑的东西都属于一个模型,无论是数据库查询、计算、REST调用等等。

你可以在模型本身中访问数据,MVC模式并没有限制你这么做。你可以用服务、映射器等等来美化它,但模型的实际定义是一个处理业务逻辑的层,仅此而已。它可以是一个类,一个函数,或者一个包含无数对象的完整模块,如果你需要的话。

有一个单独的对象来实际执行数据库查询,而不是让它们直接在模型中执行:这在单元测试时尤其方便(因为在你的模型中容易注入模拟数据库依赖项):

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中,您很少需要捕获/重新抛出异常,因为保留了反向跟踪,特别是在像您的示例这样的情况下。只是让异常抛出并在控制器中捕获它。

在我的例子中,我有一个数据库类,它处理所有直接的数据库交互,如查询、获取等。因此,如果我必须将数据库从MySQL更改为PostgreSQL,就不会有任何问题。所以增加额外的一层是有用的。

每个表可以有自己的类和特定的方法,但要实际获取数据,它让数据库类处理它:

文件# EYZ0

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;
}
}

表对象类

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);
}


....
}

我希望这个例子能帮助您创建一个好的结构。

免责声明:以下是我如何在基于php的web应用程序的上下文中理解类似mvc的模式的描述。所有在内容中使用的外部链接都是为了解释术语和概念,不是暗示我自己在这个主题上的可信度。

我必须澄清的第一件事是:模型是一个层

其次,经典MVC和我们在web开发中使用的是有区别的。这是是我以前写的一个答案,简要地描述了它们的不同之处。

模型不是什么:

模型不是一个类或任何单个对象。使用(我也是,虽然最初的答案是在我开始学习的时候写的)是一个非常常见的错误,因为大多数框架都延续了这个误解。

它既不是对象关系映射技术(ORM),也不是数据库表的抽象。任何不这么认为的人都很可能是在尝试“卖出”另一个全新的ORM或整个框架。

什么是模型:

在适当的MVC改编中,M包含了所有的域业务逻辑,模型层主要是,由三种类型的结构组成:

  • < p > # EYZ0

    域对象是纯域信息的逻辑容器;它通常表示问题域空间中的一个逻辑实体。通常称为业务逻辑

    在这里,您可以定义如何在发送发票之前验证数据,或者如何计算订单的总成本。与此同时,域对象完全不知道存储-无论是从在哪里 (SQL数据库,REST API,文本文件等),甚至如果,他们都被保存或检索

  • < p > # EYZ0

    这些对象只负责存储。如果您将信息存储在数据库中,则SQL将驻留在数据库中。或者您可能使用XML文件存储数据,而数据映射器正在从XML文件解析到XML文件

  • < p > # EYZ0

    你可以把它们看作是“高级域对象”,但不是业务逻辑,服务负责域对象映射器之间的交互。这些结构最终创建了一个用于与域业务逻辑交互的“公共”接口。您可以避免它们,但代价是将一些域逻辑泄漏到控制器中。

    ACL实现问题中有一个与此主题相关的答案-它可能有用

模型层和MVC三元组的其他部分之间的通信只能通过服务进行。这种明确的分离还有一些额外的好处:

  • 它有助于执行单一责任原则 (SRP)
  • 在逻辑发生变化时提供额外的“摆动空间”
  • 保持控制器尽可能简单
  • 如果你需要一个外部API,它会给出一个清晰的蓝图

,

如何与模型交互?

<强>先决条件:观看讲座"全局状态和单例""不找东西!" . 0 from the Clean Code Talks. . 0 from the Clean Code Talks. . 0

获得对服务实例的访问权

对于视图控制器实例(你可以称之为:“UI层”)都可以访问这些服务,有两种一般方法:

  1. 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用DI容器。
  2. 将服务的工厂用作所有视图和控制器的强制依赖项。

正如您可能怀疑的那样,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分离

这意味着,当您的表示层逻辑几乎不存在时,实用的方法是将它们保持为单层。它还可以极大地简化模型层的某些方面。

使用这种方法,登录示例(对于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方法中。

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}

创建映射器的方法

要实现持久性的抽象,最灵活的方法是创建自定义的数据映射器

Mapper diagram

From: PoEAA book

实际上,它们是为了与特定的类或超类进行交互而实现的。假设您的代码中有CustomerAdmin(两者都继承自User超类)。两者可能最终都有一个单独的匹配映射器,因为它们包含不同的字段。但是您最终还将得到共享的和常用的操作。例如:更新“最后在网上见到”时间。而不是让现有的映射器更复杂,更实用的方法是有一个通用的“用户映射器”,它只更新时间戳。

其他一些意见:

  1. < p > # EYZ0

    虽然有时在数据库表域对象映射器之间有直接的1:1:1的关系,但在较大的项目中,这种关系可能不像你想象的那么常见:

    • 单个域对象所使用的信息可能来自不同的表,而对象本身在数据库中没有持久性。

      例子:,如果您正在生成月度报告。这将从不同的表中收集信息,但数据库中没有神奇的MonthlyReport

    • 单个映射器可以影响多个表。

      当您存储来自User对象的数据时,这个域对象可以包含其他域对象的集合- Group实例。如果你改变它们并存储User数据映射器将不得不更新和/或在多个表中插入条目

    • 单个域对象中的数据存储在多个表中。

      例子:在大型系统中(想想:一个中等规模的社交网络),将用户身份验证数据和经常访问的数据与很少需要的大块内容分开存储可能是实用的。在这种情况下,你可能仍然有一个单一的User类,但它包含的信息将取决于是否获取完整的细节

    • 对于每个域对象可以有多个映射器

      例子:,你有一个基于面向公众和管理软件的共享代码的新闻网站。但是,虽然两个接口都使用相同的Article类,但管理需要在其中填充更多的信息。在这种情况下,你会有两个独立的映射器:“internal”和“external”。每个执行不同的查询,甚至使用不同的数据库(如主数据库或从数据库)

    • 李< / ul > < / >
    • < p > # EYZ0

      MVC中的视图实例(如果你没有使用模式的MVP变体)负责表示逻辑。这意味着每个视图通常会处理至少几个模板。它从模型层中获取数据,然后根据接收到的信息选择模板并设置值。

      您从中获得的好处之一是可重用性。如果您创建了ListView类,那么,通过编写良好的代码,您可以让相同的类处理文章下面的用户列表和注释的表示。因为它们都有相同的表示逻辑。您只需切换模板。

      您可以使用原生PHP模板或使用一些第三方模板引擎。也可能有一些第三方库,它们能够完全取代视图实例

    • < p > # EYZ0

      唯一的主要变化是,旧版本中所谓的模型实际上是服务。“图书馆类比”的其余部分保持得很好。

      我看到的唯一缺陷是,这将是一个非常奇怪的库,因为它会从书中返回信息,但不让你触摸书本身,否则抽象就会开始“泄漏”。我可能不得不想出一个更合适的类比

    • < p > # EYZ0

      MVC结构由用户界面和模型两层组成。用户界面层中的主要结构是视图和控制器。

      当你在处理使用MVC设计模式的网站时,最好的方法是视图和控制器之间有1:1的关系。每个视图都代表你网站中的一个完整页面,它有一个专用的控制器来处理该特定视图的所有传入请求。

      例如,要表示打开的文章,可以使用\Application\Controller\Document\Application\View\Document。这将包含UI层的所有主要功能,当涉及到处理文章(当然你可能有一些XHR组件,这些组件与文章没有直接关系).

通常情况下,大多数应用程序都有数据、显示和处理部分,我们只是把它们都放在字母MVC中。

拥有持有应用程序状态的属性,它不知道关于VC的任何事情。

视图(# EYZ0)—>有显示格式的应用程序和只知道如何消化模型上,不打扰C

控制器(# EYZ0)---->具有应用程序的处理部分,并充当M和V之间的接线,它取决于MV,而不像MV

总之,每个人之间的关注点是分离的。 在未来的任何变化或增强可以很容易地添加