如何在MVC中构建模型?

Die*_*xel 541 php architecture oop model-view-controller model

我只是掌握了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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我最终会有四层吗?

ter*_*ško 893

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

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

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

什么样的模型不是:

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

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

模特是什么:

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

  • 域对象

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

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

  • 数据映射器

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

  • 服务

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

    ACL实现问题中,这个主题有一个相关的答案- 它可能很有用.

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

  • 它有助于执行单一责任原则(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')
    );
}
Run Code Online (Sandbox Code Playgroud)

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

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

向用户显示状态变化.

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

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

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

表示层实际上可以非常精细,如下所述:了解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);
}
Run Code Online (Sandbox Code Playgroud)

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

 

如何建立模型?

由于没有单一的"模型"类(如上所述),因此您实际上并不"构建模型".相反,您可以从制作能够执行某些方法的服务开始.然后实现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();
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,在此抽象级别,没有指示数据从何处获取.它可能是一个数据库,但它也可能只是一个用于测试目的的模拟对象.即使是实际用于它的数据映射器也隐藏在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);
}
Run Code Online (Sandbox Code Playgroud)

创建映射器的方法

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

映射图

来自:PoEAA

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

一些额外的评论:

  1. 数据库表和模型

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

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

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

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

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

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

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

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

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

  2. 视图不是模板

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

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

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

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

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

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

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

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

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

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

  • 很好地看到他发明了MVC这篇文章可能有一些优点. (9认同)
  • @hafichuk只是情况,当合理地采用[*ActiveRecord*](http://martinfowler.com/eaaCatalog/activeRecord.html)模式进行原型设计时.当您开始编写适合生产的代码时,它就变成了反模式,因为它混合了存储和业务逻辑.因为*Model Layer*完全没有意识到其他MVC部分.**这不会因原始图案**的变化而改变.即使使用MVVM.**没有"多个模型",它们没有映射到任何东西.模型是一层.** (8认同)
  • @Rinzler,您会注意到,在该链接中没有任何地方,任何关于模型的内容(除了一条评论).它只是*"面向对象的数据库表接口"*.如果你试图在类似模型的东西中塑造它,你最终会违反[**SRP**](http://codingcraft.files.wordpress.com/2011/03/srp.jpg)和[**LSP**](http://codingcraft.files.wordpress.com/2011/03/lsp.jpg). (4认同)
  • 简短版本 - 模型是[数据结构](http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf). (3认同)
  • ......甚至只是一组功能.MVC不需要以OOP样式实现,尽管它主要以这种方式实现.最重要的是分离层并建立正确的数据和控制流程 (3认同)
  • @EddieB表示祝贺.你以某种方式设法错过了整个*'这篇文章中没有"模型"`*点.此外,虽然还不错,但本书是关于应用程序建模的(如UML和诸如此类的).它只涉及单章中的MVC. (2认同)
  • `模型不是一个类或任何单个对象.虽然我在我的项目中对此进行了确认,但我不会对此进行概括.在最简单的模型中可以只是一个类. (2认同)
  • @ hek2mgl,如果您的模型的整体可以表示为单个类(假设您遵守SRP),那么您不应该使用MVC开始.它不是"hello world"规模应用的模式.在这种情况下,您应该只遵循特定应用程序中编程风格的一般"良好实践"(无论是程序性的,面向对象的,功能性的还是其他的).在这种情况下应用完全实现的MVC是浪费资源(除非您正在研究). (2认同)
  • 那个聊天线看起来非常清晰,而且大多是错误的**(它也是两年前)**. (2认同)
  • @MajoB"viewmodel"是用MVVM架构模式替换控制器的东西.您可能是指[演示文稿对象](http://martinfowler.com/eaaDev/PresentationModel.html).那些我并不认为你会直接与域对象链接的东西,因为我不希望域对象暴露给模型层之外的任何东西. (2认同)

net*_*der 36

作为业务逻辑的一切都属于模型,无论是数据库查询,计算,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');
Run Code Online (Sandbox Code Playgroud)

此外,在PHP中,您很少需要捕获/重新抛出异常,因为保留了回溯,特别是在您的示例中.只是抛出异常并在控制器中捕获它.

  • -1:它也恰好是完全错误的.模型不是表的抽象. (10认同)

mar*_*rio 20

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

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

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

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

  • 链接无效(404) (8认同)

fee*_*ing 6

更常见的是,大多数应用程序都有数据,显示和处理部分,我们只是将所有这些应用程序放在字母中M,V并且C.

Model(M) - >具有保存应用程序状态的属性,它不知道任何关于V和的事情C.

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

Controller(C) ---->具有应用程序的处理部分,作为M和V之间的接线,并且它依赖于两者M,V不像MV.

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