首页 文章

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

提问于
浏览
524

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

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

模型对象是否应具有所有数据库映射属性以及上面的代码,或者可以将实际执行数据库工作的代码分开吗?

我最终会有四层吗?

5 回答

  • 19

    business logic 的所有内容都属于模型,无论是数据库查询,计算,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中,您很少需要捕获/重新抛出异常,因为保留了回溯,特别是在您的示例中 . 只是抛出异常并将其捕获到控制器中 .

  • 5

    更常见的是,大多数应用程序都有数据,显示和处理部分,我们只需将所有这些应用程序放在 MVC 中 .

    模型( M ) - >具有保持应用状态的属性,它不知道关于 VC 的任何事情 .

    查看( V ) - >显示应用程序的格式,并且只知道如何在其上消化模型,并且不关心 C .

    控制器( C )---->具有应用程序的处理部分,作为M和V之间的接线,它与 MV 不同,与 MV 不同 .

    总之,每个人之间存在着分离的关注 . 将来可以非常轻松地添加任何更改或增强功能 .

  • 33

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

    我必须清楚的第一件事是: the model is a layer .

    第二:经典MVC与我们在Web开发中使用的内容之间存在差异 . Here's我写的一个较旧的答案,简要描述了它们的不同之处 .

    什么型号不是:

    该模型不是类或任何单个对象 . 这是一个非常常见的错误(我也这样做,虽然原来的答案是在我开始学习的时候写的),因为大多数框架都会延续这种误解 .

    它既不是对象关系映射技术(ORM)也不是数据库表的抽象 . 任何告诉你的人最有可能尝试另一个全新的ORM或整个框架 .

    模特是什么:

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

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

    您可以在此处定义如何在发送发票之前验证数据,或计算订单的总成本 . 同时,Domain Objects完全不知道存储 - 既不是从哪里(SQL数据库,REST API,文本文件等),也不是它们被保存或检索 .

    这些对象仅负责存储 . 如果将信息存储在数据库中,这将是SQL所在的位置 . 或者您可能使用XML文件来存储数据,而您的数据映射器正在解析XML文件 .

    您可以将它们视为"higher level Domain Objects",但不是业务逻辑,服务负责域对象和映射器之间的交互 . 这些结构最终创建了一个"public"接口,用于与域业务逻辑进行交互 . 您可以避免使用它们,但会将某些域逻辑泄漏到控制器中 .

    ACL implementation问题中有关于此主题的相关答案 - 它可能有用 .

    模型层与MVC三元组其他部分之间的通信应仅通过Services进行 . 明确的分离有一些额外的好处:

    • 有助于强制执行single responsibility principle(SRP)
      如果逻辑发生变化,

    • 会提供额外的'wiggle room'

    • 使控制器尽可能简单
      如果您需要外部的话,

    • 会给出一个清晰的蓝图API

    如何与模型互动?

    先决条件:观看讲座“全球状态和单身人士”和“不要寻找事物!”来自清洁代码会谈 .

    获取对服务实例的访问权限

    对于View和Controller实例(您可以调用的内容:"UI layer")来访问这些服务,有两种常规方法:

    • 您可以直接在视图和控制器的构造函数中注入所需的服务,最好使用DI容器 .

    • 使用工厂将服务作为所有视图和控制器的强制依赖项 .

    正如您可能怀疑的那样,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')
        );
    }
    

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

    Controller不负责验证用户的输入,因为这是业务规则的一部分,控制器绝对不会调用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中它将用户输入和表示的处理分开 . 这很关键 . 虽然人们经常将其描述为"triad",但它实际上并不是由三个独立的部分组成 . 结构更像是这样的:

    MVC separation

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

    使用此方法,登录示例(对于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);
    }
    

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

    如何 Build 模型?

    由于没有一个"Model"类(如上所述),你真的不"build the model" . 相反,您可以从制作能够执行某些方法的服务开始 . 然后实现Domain Objects和Mappers .

    服务方法的示例:

    在上述两种方法中,都有这种识别服务的登录方法 . 它实际上会是什么样子 . 我正在使用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 .

    Mapper diagram

    来自:PoEAA书

    实际上,它们是为与特定类或超类的交互而实现的 . 假设你的代码中有 CustomerAdmin (都继承自 User 超类) . 两者都可能最终有一个单独的匹配映射器,因为它们包含不同的字段 . 但是,您最终还将获得共享和常用操作 . 例如:更新"last seen online"时间 . 而不是让现有的映射器更复杂,更实用的方法是使用通用"User Mapper",它只更新该时间戳 .

    一些其他评论:

    • Database tables and model

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

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

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

    • 单个Mapper可以影响多个表 .

    示例:当您从 User 对象存储数据时,此域对象可能包含其他域对象的集合 - Group 实例 . 如果更改它们并存储 User ,则Data Mapper必须更新和/或插入多个表中的条目 .

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

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

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

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

    • A view is not a template

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

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

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

    • What about the old version of the answer?

    唯一的主要变化是,在旧版本中称为Model,实际上是一个服务 . "library analogy"的其余部分保持良好状态 .

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

    • What is the relationship between View and Controller instances?

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

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

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

  • 0

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

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

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

    无论哪种方式,如果将SQL内容或数据库调用分成另一层,那么你就不是很远了 . 这样,您只需关注真实的数据/行为,而不是实际的存储API . (然而,过度使用它是不合理的 . 如果没有提前设计,你将永远无法用文件存储替换数据库后端 . )

  • 844

    在我的情况下,我有一个数据库类,处理所有直接数据库交互,如查询,提取等 . 因此,如果我必须将我的数据库从MySQL更改为PostgreSQL,则不会有任何问题 . 因此添加额外的图层会很有用 .

    每个表都可以拥有自己的类并具有其特定的方法,但是为了实际获取数据,它允许数据库类处理它:

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

    表对象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);
        }
    
        ....
    }
    

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

相关问题