【问题标题】:How should a model be structured in MVC? [closed]在 MVC 中应该如何构建模型? [关闭]
【发布时间】:2011-08-17 08:35:45
【问题描述】:

我刚刚掌握了 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;
    }
}

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

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

我最终会有四层吗?

【问题讨论】:

  • 为什么捕获异常只是为了再次抛出它们?
  • @Elias Van Ootegem:嗯?如果它与 rethrow 一起工作,则意味着上层捕获了异常。但是如果有,那么它会在没有那个毫无意义的重新抛出的情况下捕获它......(如果你仍然没有得到它,请模拟一个小测试代码)
  • @Elias Van Ootegem:我不知道你在说什么,不在特定层处理异常并不意味着它会停止应用程序。请构造(或更准确地说:构造失败)一个需要重新抛出的代码示例。让我们停止这个离题的谈话,拜托
  • @drrcknlsn:这是一个有效的论点,但在这种情况下,至少要捕获您期望抛出的异常,通用的 Exception 没有太多的文档价值。就我个人而言,如果我走上这条路,我会选择 PHPDoc 的@exception,或者一些类似的机制,所以它会显示在生成的文档中。

标签: php oop model-view-controller architecture model


【解决方案1】:

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

首先我必须澄清的是:模型是一个层

第二:经典的 MVC 和我们在 Web 开发中使用的有区别。 Here's 我写的一个较旧的答案,简要描述了它们的不同之处。

什么不是模型:

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

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

什么是模型:

在适当的 MVC 适配中,M 包含所有领域业务逻辑,模型层大部分由三种类型的结构组成:

  • Domain Objects

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

    您可以在此处定义如何在发送发票之前验证数据或计算订单的总成本。同时,域对象完全不知道存储——既不来自where(SQL数据库、REST API、文本文件等),甚至也不来自if 它们被保存或检索。

  • Data Mappers

    这些对象只负责存储。如果您将信息存储在数据库中,这将是 SQL 所在的位置。或者您可能使用 XML 文件来存储数据,并且您的 Data Mappers 正在解析 XML 文件。

  • Services

    您可以将它们视为“更高级别的域对象”,但服务负责域对象映射器之间的交互,而不是业务逻辑/em>。这些结构最终创建了一个用于与域业务逻辑交互的“公共”接口。您可以避免它们,但代价是会将一些域逻辑泄漏到 Controllers

    ACL implementation 问题中有与此主题相关的答案 - 它可能很有用。

模型层与 MVC 三元组的其他部分之间的通信只能通过 Services 进行。清晰的分离还有一些额外的好处:

  • 这有助于强制执行 single responsibility principle (SRP)
  • 在逻辑发生变化时提供额外的“回旋余地”
  • 使控制器尽可能简单
  • 提供清晰的蓝图,如果您需要外部 API

 

如何与模型交互?

先决条件:观看 Clean Code Talks 中的 "Global State and Singletons""Don't Look For Things!" 讲座。

获得对服务实例的访问权

对于 ViewController 实例(您可以称之为:“UI 层”)访问这些服务,有两种通用方法:

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

您可能会怀疑,DI 容器是一个更优雅的解决方案(但对于初学者来说并不是最简单的)。我建议考虑使用此功能的两个库是 Syfmony 的独立库 DependencyInjection componentAuryn

使用工厂和 DI 容器的解决方案还允许您共享各种服务器的实例,以便在给定的请求-响应周期内在选定的控制器和视图之间共享。

模型状态的改变

现在您可以访问控制器中的模型层,您需要开始实际使用它们:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

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

控制器不负责验证用户的输入,因为这是业务规则的一部分,控制器绝对不会调用 SQL 查询,就像你看到的 herehere (请不要讨厌他们,他们被误导,而不是邪恶)。

向用户显示状态变化。

好的,用户已登录(或失败)。 Now what? 说用户还不知道。因此,您需要实际产生响应,这是视图的责任。

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

在这种情况下,视图会根据模型层的当前状态产生两种可能的响应之一。对于不同的用例,您可以让视图根据“当前选择的文章”之类的内容选择不同的模板进行渲染。

表示层实际上可以变得非常复杂,如下所述:Understanding MVC Views in PHP

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

当然,在某些情况下,这是一种矫枉过正的情况。

MVC 只是Separation of Concerns 原则的具体解决方案。 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);
}

虽然这是不可持续的,但当您有复杂的逻辑来呈现响应正文时,这种简化对于更琐碎的场景非常有用。但是警告,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦。

 

如何构建模型?

由于没有单一的“模型”类(如上所述),因此您实际上并没有“构建模型”。相反,您从制作服务开始,这些服务能够执行某些方法。然后实现Domain ObjectsMappers

服务方法示例:

在上述两种方法中,识别服务都有这种登录方法。它实际上会是什么样子。我正在使用 a library 中相同功能的略微修改版本,我写的..因为我很懒:

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

创建映射器的方法

要实现持久性的抽象,最灵活的方法是创建自定义data mappers

发件人:PoEAAbook

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

一些额外的 cmets:

  1. 数据库表和模型

    虽然有时数据库表、域对象Mapper 之间存在直接的 1:1:1 关系,但在较大的项目中它可能不如您常见期望:

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

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

    • 一个Mapper可以影响多个表。

      示例:当您存储来自User 对象的数据时,此域对象 可能包含其他域对象的集合 - Group 实例。如果您更改它们并存储 UserData Mapper 将不得不更新和/或在多个表中插入条目。

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

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

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

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

  2. 视图不是模板

    MVC 中的

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

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

    您可以使用native PHP templates 或使用一些第三方模板引擎。也可能有一些第三方库,它们能够完全替换 View 实例。

  3. 旧版答案呢?

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

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

  4. ViewController实例有什么关系?

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

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

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

【讨论】:

  • @Rinzler ,您会注意到,在该链接中,没有任何关于 Model 的内容(除了一条评论)。它只是“数据库表的面向对象接口”。如果你试图把它塑造成一个类似于模型的东西,你最终会违反SRPLSP
  • @hafichuk 仅在合理使用ActiveRecord 模式的情况下用于原型设计。当您开始编写对生产有意义的代码时,它就变成了一种反模式,因为它混合了存储和业务逻辑。而且由于 Model Layer 完全不知道其他 MVC 部分。 这不会因原始模式的变化而改变。即使在使用 MVVM 时。 没有“多个模型”,它们没有映射到任何东西。模型是一个层。
  • 短版 - 型号为Data Structures
  • 好吧,看到他发明了 MVC,这篇文章可能有一些优点。
  • ... 甚至只是一组函数。 MVC 不需要以 OOP 风格实现,尽管它主要是以这种方式实现的。最重要的是分离层并建立正确的数据和控制流
【解决方案2】:

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

【讨论】:

  • 我的结构很相似,我想我只是把它分开了一点。我绕过连接的原因是因为我需要在事务中运行块。我想添加一个用户,然后将用户添加到一个角色,但如果一个失败,角色会返回。我能解决这个问题的唯一方法是通过连接。
  • -1:它也恰好是完全错误的。模型不是表的抽象。
  • User 类基本上扩展了模型,但它不是对象。用户应该是一个对象并具有如下属性:id、name ...您正在部署 User 类是一个助手。
  • 我想你了解 MVC 但不了解什么是 OOP。在这种情况下,就像我说的,User 代表一个对象,它应该有一个用户的属性,而不是像CheckUsername 这样的方法,如果你想创建新的User 对象应该怎么做? new User($db)
  • @TomSawyer OOP 并不意味着对象必须具有属性。您所描述的是一种设计模式,与问题或该问题的答案无关。 OOP 是一种语言模型,而不是一种设计模式。
【解决方案3】:

大多数应用程序通常都有数据、显示和处理部分,我们只是将所有这些都放在字母MVC 中。

Model(M)-->具有保存应用程序状态的属性,它对VC一无所知。

View(V)-->有应用程序的显示格式,只知道如何消化模型,不关心C

Controller(C)---->具有应用程序的处理部分并充当 M 和 V 之间的接线,它依赖于 M,VM 不同和V

总的来说,每个人之间存在关注点分离。 将来可以很容易地添加任何更改或增强功能。

【讨论】:

    【解决方案4】:

    在 Web-“MVC”中,你可以为所欲为。

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

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

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

    【讨论】:

    【解决方案5】:

    在我的例子中,我有一个数据库类来处理所有直接的数据库交互,例如查询、获取等。因此,如果我必须将我的数据库从 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);
        }
    
        ....
    }
    

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

    【讨论】:

    • "因此,如果我必须将数据库从 MySQL 更改为 PostgreSQL,则不会有任何问题。" Uhhhmmm 使用上面的代码,改变任何 imo 都会有很大的问题。
    • 我发现我的答案在编辑后变得越来越没有意义,而且随着时间的推移。但它应该留在这里
    • Database 在示例中不是一个类。它只是函数的包装器。另外,没有对象怎么能有“表对象类”?
    • @tereško 我已经阅读了您的许多帖子,它们都很棒。但是,我在任何地方都找不到任何完整的框架来学习。你知道有一个“做对了”吗?或者至少有一个喜欢你和这里的其他人这么说的吗?谢谢。
    • 我可能迟到了,但我想指出 PDO 几乎解决了必须创建数据库“层”以促进未来更改的问题。
    猜你喜欢
    • 1970-01-01
    • 2013-01-23
    • 2021-09-11
    • 2013-07-18
    • 2021-04-06
    • 1970-01-01
    • 1970-01-01
    • 2023-03-31
    相关资源
    最近更新 更多