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三元组的其他部分之间的通信只能通过服务进行。 明确的分离有一些额外的好处:

  • 它有助于执行单一责任原则(SRP)
  • 在逻辑改变的情况下提供额外的“摆动室”
  • 尽可能简化控制器
  • 给出了一个清晰的蓝图,如果你需要一个外部的API
  • 如何与模型进行交互?

    先决条件:观看“全球州和单身人士”讲座和“不要找东西!” 来自Clean Code Talks。

    获得服务实例的访问权限

    对于View和Controller实例(您可以调用:“UI层”)来访问这些服务,有两种常用方法:

  • 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用DI容器。
  • 使用工厂作为所有视图和控制器的必需依赖项。
  • 正如你可能会怀疑的那样,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将用户界面与业务逻辑分离开来,并在用户界面中将用户输入和演示分开处理。 这是至关重要的。 虽然人们经常把它形容为“黑社会”,但实际上并不是由三个独立的部分组成。 结构更像这样:

    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书

    在实践中,它们被实现用于与特定类别或超类的交互。 假设你的代码中有CustomerAdmin (都是从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的关系。 每个视图都代表您网站中的整个页面,并且它具有专用控制器来处理该特定视图的所有传入请求。

    例如,要表示已打开的文章,您应该有ApplicationControllerDocumentApplicationViewDocument 。 这将包含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

    上一篇: How should a model be structured in MVC?

    下一篇: textarea with @Html.EditorFor