提问



我只是掌握了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 与我们在Web开发中使用的内容之间存在差异。这是我写的一个较旧的答案,简要描述了它们的不同之处。


什么样的模型不是:



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


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


模特是什么:



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



  • 域对象 [38]



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



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

  • 数据映射器 [39]


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

  • 服务 [40]


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


    在ACL实现问题中有一个相关的答案 - 它可能是有用的。



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



  • 有助于执行单一责任原则(SRP)

  • 在逻辑更改
  • 的情况下提供额外的摆动空间
  • 让控制器尽可能简单

  • 如果您需要外部API,
  • 会给出清晰的蓝图



  [42]


如何与模型互动?




   先决条件:观看讲座全球状态和单身人士和不要寻找事情!来自清除代码会谈。 [43] [44]



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



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



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

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



正如您可能怀疑的那样,DI容器是一个更优雅的解决方案(虽然对初学者来说不是最简单的)。我建议考虑使用这个功能的两个库是Syfmony的独立DependencyInjection组件或Auryn。[45] [46]


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


模型状态的改变



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


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


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


控制器不负责验证用户的输入,因为这是业务规则的一部分,控制器肯定不会调用SQL查询,就像你在这里或这里看到的那样(请不要讨厌它们,它们是误导的,不是邪恶的)。


向用户显示状态更改。



好的,用户已登录(或失败)。怎么办?所述用户仍然没有意识到它。因此,您需要实际生成响应,这是视图的责任。 [49]


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


在这种情况下,视图基于模型层的当前状态产生两个可能响应之一。对于不同的用例,您可以根据当前选定的文章等内容选择要渲染的不同模板。


表示层实际上可以非常精细,如下所述:了解PHP中的MVC视图。


但我只是制作一个REST API!



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


MVC只是分离关注原则的具体解决方案。 MVC将用户界面与业务逻辑分开,在UI中它将用户输入和表示的处理分开。这是至关重要的。虽然人们经常把它形容为三合会,但它实际上并不是由三个独立的部分组成。结构更像是这样:[51]





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


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


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


 


如何构建模型?



由于没有单一的模型类(如上所述),因此您实际上并不构建模型。相反,您可以从制作能够执行某些方法的服务开始。然后实现域对象和 Mappers 。


服务方法的一个例子:



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


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


创建映射器的方法



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





来自:PoEAA书 [54]


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


其他一些评论:




  1. 数据库表和模型


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



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


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

    • 单个映射器会影响多个表。


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

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


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

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


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


  2. 视图不是模板


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


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


    您可以使用本机PHP模板或使用某些第三方模板引擎。也可能有一些第三方库,它们能够完全取代 View 实例。[55]

  3. 旧版本的答案怎么样?


    唯一的主要变化是,旧版本中所谓的 Model 实际上是 Service 。 库类比的其余部分保持良好状态。


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

  4. 查看和控制器实例之间的关系是什么?


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


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


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


其它参考1


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

其它参考2


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


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


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


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

其它参考3


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


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


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


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


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

其它参考4


在我的情况下,我有一个数据库类,处理所有直接数据库交互,如查询,提取等。因此,如果我不得不将我的数据库从MySQL更改为PostgreSQL,那么就不会有任何问题。所以添加额外的层可能很有用。[58] [59]


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


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

    ....
}


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