在MVC中应如何构建模型?[关闭]


551

我只是对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;
    }
}

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

该模型对象应该具有所有数据库映射的属性以及上面的代码,还是可以将代码分开以使数据库真正起作用?

我最终会有四层吗?


133
为什么要捕获异常只是为了再次抛出它们?
Bailey Parker

9
@Elias Van Ootegem:您错过了重点。在这种情况下抓住它们是没有意义的。
Karoly Horvath'7

4
@Elias Van Ootegem:是吗?如果它与重新抛出一起工作,则意味着上层捕获了异常。但是,如果有一个,那么它会毫无意义地被重新捕获……(如果您仍然没有得到它,请模拟一个小的测试代码)
Karoly Horvath

3
@Elias Van Ootegem:我不知道您在说什么,不处理特定层上的异常并不意味着它将停止应用程序。请构造(或更准确地说:无法构造)需要重新抛出的代码示例。让我们停止这场
脱口而出的

6
@drrcknlsn:这是一个有效的参数,但是在这种情况下,至少可以捕获您期望引发的异常,泛型Exception没有太多的文档价值。就个人而言,如果我走这条路,我会选择PHPDoc的@exception或某种类似的机制,因此它会显示在生成的文档中。
卡洛里·霍瓦斯

Answers:


903

免责声明:以下内容描述了如何在基于PHP的Web应用程序上下文中理解类似MVC的模式。内容中使用的所有外部链接都在这里解释术语和概念,而不是暗示我在该主题上的信誉。

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

第二:经典MVC与我们在Web开发中使用的MVC之间存在差异。这是我写的一个较旧的答案,简要说明了它们的不同之处。

什么不是模型:

该模型不是类或任何单个对象。这是一个非常常见的错误(尽管最初的答案是在我开始学习其他方法时写的,但我也这样做了),因为大多数框架都会使这种错误观念永久存在。

它既不是对象关系映射技术(ORM),也不是数据库表的抽象。否则,任何告诉您的人都极有可能试图“出售”另一个全新的ORM或整个框架。

什么是模型:

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

  • 域对象

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

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

  • 数据映射器

    这些对象仅负责存储。如果您将信息存储在数据库中,则SQL便会存在。或者,也许您使用XML文件存储数据,并且数据映射器正在XML文件之间进行解析。

  • 服务

    您可以将它们视为“高级域对象”,但服务负责域对象Mappers之间的交互,而不是业务逻辑。这些结构最终创建了一个“公共”接口,用于与域业务逻辑进行交互。您可以避免使用它们,但是要付出一些将域逻辑泄漏到Controllers中的代价。

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

模型层和MVC三合一其他部分之间的通信应仅通过Services进行。明确的分离还有一些其他好处:

  • 它有助于实施单一责任原则(SRP)
  • 提供额外的“摆动空间”,以防逻辑更改
  • 使控制器尽可能简单
  • 如果您需要外部API,则提供清晰的蓝图

 

如何与模型互动?

先决条件:观看讲座“全球状态和单例”“别找东西!” 从“干净代码讨论”中获得。

访问服务实例

对于要访问这些服务的ViewController实例(可以称为“ 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')
    );
}

您的控制器有一个非常明确的任务:接受用户输入,并基于此输入更改业务逻辑的当前状态。在此示例中,在“匿名用户”和“已登录用户”之间切换的状态。

Controller不负责验证用户的输入,因为这是业务规则的一部分,并且Controller绝对不调用SQL查询,就像您在此处此处所看到的(请不要讨厌它们,它们被误导了,不是邪恶的)。

向用户显示状态更改。

好的,用户已登录(或失败)。怎么办?该用户仍未意识到这一点。因此,您需要实际产生响应,这是视图的责任。

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

在这种情况下,视图基于模型层的当前状态生成了两个可能的响应之一。对于不同的用例,您将基于“当前选定的文章”之类的视图,选择不同的模板进行呈现。

表示层实际上可以变得非常复杂,如此处所述:了解PHP中的MVC视图

但是我只是制作一个REST API!

当然,在某些情况下,这是一个过大的杀伤力。

MVC只是关注点分离原则的具体解决方案。MVC将用户界面与业务逻辑分开,并且在UI中将用户输入和表示的处理分开。这很关键。人们通常将其描述为“三合会”,但实际上并非由三个独立的部分组成。结构更像这样:

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

尽管这是不可持续的,但是当您具有用于呈现响应主体的复杂逻辑时,这种简化对于更琐碎的场景非常有用。但请注意,当尝试在具有复杂表示逻辑的大型代码库中使用该方法时,它将成为一场噩梦。

 

如何建立模型?

由于没有单个“模型”类(如上所述),因此您实际上不会“构建模型”。取而代之的是从制作Services,它们能够执行某些方法。然后实现Domain ObjectsMappers

服务方法的示例:

在上述两种方法中,都有用于身份验证服务的登录方法。实际会是什么样子。我使用的是我写的.. 库中相同功能的稍作修改的版本,因为我很懒:

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

创建映射器的方法

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

映射器图

来自:PoEAA

实际上,它们是为与特定类或超类进行交互而实现的。可以说您拥有CustomerAdmin在您的代码中(都继承自User超类)。由于它们包含不同的字段,因此两者可能最终都将拥有单独的匹配映射器。但是您还将最终获得共享的和常用的操作。例如:更新“最近一次在线查看”时间。而不是使现有的映射器更加复杂,更实用的方法是使用通用的“用户映射器”,该用户映射器仅更新该时间戳。

一些其他评论:

  1. 数据库表和模型

    尽管有时在大型项目中,数据库表,Domain ObjectMapper之间存在直接的1:1:1关系,但它可能不如您预期的那么普遍:

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

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

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

      示例:当您存储User对象中的数据时,该域对象可能包含其他域对象(Group实例)的集合。如果您更改它们并存储User,则数据映射器将必须在多个表中更新和/或插入条目。

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

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

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

      示例:您有一个新闻站点,该站点具有面向公众和管理软件的共享代码。但是,尽管两个接口都使用同一Article类,但管理人员需要在其中填充更多信息。在这种情况下,您将有两个单独的映射器:“内部”和“外部”。每个执行不同的查询,甚至使用不同的数据库(如在主数据库或从数据库中)。

  2. 视图不是模板

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

    您从中获得的好处之一就是可重用性。如果您创建一个ListView类,则可以使用编写良好的代码来使同一类负责在文章下方呈现用户列表和注释。因为它们都有相同的表示逻辑。您只需切换模板。

    您可以使用本机PHP模板,也可以使用某些第三方模板引擎。可能还有一些第三方库可以完全替换View实例。

  3. 那答案的旧版本呢?

    唯一的主要变化是,在旧版本中称为Model的实际上是Service。其余的“图书馆类比”保持良好。

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

  4. ViewController实例之间是什么关系?

    MVC结构由两层组成:UI和模型。UI层的主要结构是视图和控制器。

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

    例如,要表示已打开的文章,您将具有\Application\Controller\Document\Application\View\Document。在处理文章时,这将包含UI层的所有主要功能(当然,您可能有一些与文章不直接相关的XHR组件)


4
@Rinzler,您会注意到,在该链接中没有任何地方提到Model(除了一条评论)。它只是“数据库表的面向对象的接口”。如果您尝试用类似Model的方式进行建模,那么最终会违反SRPLSP
tereško

8
@hafichuk仅在合理的情况下采用ActiveRecord模式用于原型的情况。当您开始编写用于生产的代码时,由于混合了存储和业务逻辑,因此它将成为反模式。而且由于“ 模型层”完全不了解其他MVC部分。这不会根据原始图案的变化而改变。即使使用MVVM。没有“多个模型”,它们也没有映射到任何东西。模型是一层。
tereško

3
简短版本-模型是数据结构
Eddie B

9
很高兴看到他发明了MVC这篇文章可能有一些优点。
Eddie B

3
...甚至只是一组功能。MVC不需要以OOP样式实现,尽管大多数情况下是以这种方式实现的。最重要的是分离各层并建立正确的数据和控制流
hek2mgl

37

一切业务逻辑都属于模型,无论是数据库查询,计算,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中,因为保留了回溯,所以很少需要捕获/重新抛出异常,尤其是在类似示例的情况下。只要让异常被抛出并捕获到控制器中即可。


我的结构非常相似,我想我会将它分开一些。我绕过连接的原因是因为我需要在事务中运行块。我想添加一个用户,然后将该用户添加到一个角色,但是如果一个失败,请将该角色重新添加。我可以解决的唯一方法是通过连接。
Dietpixel

10
-1:这恰好是完全错误的。模型不是表的抽象。
–tereško2014年

1
User级基本上扩展模型,但itsn't的对象。用户应该是一个对象,并具有以下属性:id,name ...您正在部署的User类是一个帮助器。
TomSawyer

1
我认为您了解MVC,但不了解什么是OOP。在这种情况下,就像我说的那样,它User代表一个对象,它应该具有User的属性,而不是诸如之类的方法CheckUsername,如果要创建新的User对象该怎么办?new User($db)
TomSawyer '16

@TomSawyer OOP并不意味着要求对象具有属性。您要描述的是一种设计模式,该模式与问题或对该问题的答案无关。OOP是一种语言模型,而不是一种设计模式。
netcoder

20

在Web“ MVC”中,您可以随心所欲。

最初的概念(1)将模型描述为业务逻辑。它应该表示应用程序状态并强制某些数据一致性。该方法通常被称为“胖模型”。

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

无论哪种方式,如果将SQL内容或数据库调用分离到另一层中,都不会很遥远。这样,您只需要关心真实的数据/行为,而不必关心实际的存储API。(不过,这样做是不合理的。例如,如果事先没有设计,您将永远无法用filetorage替换数据库后端。)


8
链接无效(404)
Kyslik

1
这可以从WebArchive进行:web.archive.org/web/20101229204648/https
Tudor

6

更多oftenly大多数应用都会有数据,显示及处理部分,我们只是把字母所有这些MVC

Model(M ->具有保存应用程序状态的属性,并且不了解Vand C

View(V ->具有显示应用程序的格式,并且仅了解其上的操作方法模型,而无需担心C

Controller(C ---->具有处理应用程序的部分,并充当M和V之间的连线,并且取决于两者MVM和不同V

两者之间总有分离的关注点。将来,可以很容易地添加任何更改或增强。


0

就我而言,我有一个数据库类来处理所有直接的数据库交互,例如查询,提取等。因此,如果我不得不将数据库从MySQL更改为PostgreSQL,就不会有任何问题。因此,添加额外的层可能很有用。

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

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

表格对象类L

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

    ....
}

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


12
“因此,如果我必须将数据库从MySQL更改为PostgreSQL,就不会有任何问题。” 使用以上代码,更改imo将会遇到很大的问题。
PeeHaa 2012年

我看到我的答案在编辑后随着时间的流逝变得越来越没有意义。但它应该留在这里
伊布

2
Database在示例中不是类。它只是功能的包装。另外,如何在没有对象的情况下拥有“表对象类”?
tereško

2
@tereško我读了很多您的帖子,它们很棒。但是,我找不到任何可以学习的完整框架。您是否知道“做对了”?还是至少有一个像您和其他一些人所说的那样做?谢谢。
约翰尼2014年

我可能已经晚了,但我想指出的是,PDO几乎解决了必须创建数据库“层”以便于将来进行更改的问题。
马修·古拉特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.