我刚刚掌握了 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 包含所有领域业务逻辑, 模型层 主要 由 三种类型的结构组成:
域对象是纯域信息的逻辑容器;它通常代表问题域空间中的一个逻辑实体。通常称为 业务逻辑 。
您可以在此处定义如何在发送发票之前验证数据,或计算订单的总成本。同时, 域对象 完全不知道存储——既不知道从 哪里 (SQL 数据库、REST API、文本文件等),也不 知道 它们是否被保存或检索。
这些对象只负责存储。如果您将信息存储在数据库中,这将是 SQL 所在的位置。或者您可能使用 XML 文件来存储数据,并且您的 数据映射 器正在解析 XML 文件和解析 XML 文件。
您可以将它们视为“更高级别的域对象”,但 服务 不是业务逻辑,而是负责 域对象 和 映射 器之间的交互。这些结构最终创建了一个用于与域业务逻辑交互的“公共”接口。您可以避免它们,但代价是会将一些域逻辑泄漏到 Controllers 中。
在ACL implementation question中有一个与此主题相关的答案- 它可能很有用。
模型层和 MVC 三元组的其他部分之间的通信只能通过 Services 进行。清晰的分离还有一些额外的好处:
听
先决条件: 观看讲座“全球状态和单身人士”和“不要寻找东西!” 来自清洁代码会谈。
对于 View 和 Controller 实例(您可以称之为“UI 层”)访问这些服务,有两种通用方法:
您可能会怀疑,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); }
在这种情况下,视图会根据模型层的当前状态产生两种可能的响应之一。对于不同的用例,您可以让视图根据“当前选择的文章”之类的内容选择不同的模板进行渲染。
表示层实际上可以变得非常复杂,如下所述:Understanding MVC Views in PHP。
当然,在某些情况下,这是矫枉过正的。
MVC 只是关注点分离原则的具体解决方案。 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 Objects 和 Mappers 。
在上述两种方法中,识别服务都有这种登录方法。它实际上会是什么样子。我正在使用我编写的库中相同功能的略微修改版本..因为我很懒:
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
private function changeIdentityStatus(Entity\Identity $identity, int $status) { $identity->setStatus($status); $identity->setLastUsed(time()); $mapper = $this->mapperFactory->create(Mapper\Identity::class); $mapper->store($identity); }
要实现持久性的抽象,最灵活的方法是创建自定义数据映射器
在实践中,它们是为与特定类或超类交互而实现的。假设您的代码中有CustomerandAdmin在您的代码中(都继承自User超类)。两者可能最终都有一个单独的匹配映射器,因为它们包含不同的字段。但是您最终也会得到共享和常用的操作。例如:更新 “最后一次在线” 时间。而不是让现有的映射器更加复杂,更实用的方法是拥有一个通用的“用户映射器”,它只更新那个时间戳。
Customer
Admin
User
虽然有时数据库表、 Domain Object 和 Mapper 之间存在直接的 1:1:1 关系,但在较大的项目中,它可能不像您预期的那样常见:
* 单个 _域对象_ 使用的信息可能来自不同的表,而对象本身在数据库中没有持久性。
示例: 如果您正在生成月度报告。这将从不同的表中收集信息,但MonthlyReport数据库中没有神奇的表。
MonthlyReport
* 一个 _Mapper_ 可以影响多个表。
示例: 当您从User对象存储数据时,此 域对象 可能包含其他域对象的集合 -Group实例。如果您更改它们并存储User, Data Mapper 将不得不在多个表中更新和/或插入条目。
Group
* 来自单个 _域对象_ 的数据存储在多个表中。
示例: 在大型系统中(想想:一个中型社交网络),将用户身份验证数据和经常访问的数据与大块内容分开存储可能是实用的,这很少需要。在这种情况下,您可能仍然只有一个User类,但它包含的信息取决于是否获取了完整的详细信息。
* 对于每个 _域对象_ ,可以有多个映射器
示例: 您有一个新闻站点,其中包含面向公众和管理软件的共享代码。但是,虽然两个接口都使用同一个Article类,但管理需要在其中填充更多信息。在这种情况下,您将有两个独立的映射器:“内部”和“外部”。每个执行不同的查询,甚至使用不同的数据库(如在 master 或 slave 中)。
Article
MVC 中的视图 实例(如果您不使用模式的 MVP 变体)负责表示逻辑。这意味着每个 视图 通常会处理至少几个模板。它从 模型层 获取数据,然后根据收到的信息选择模板并设置值。
您从中获得的好处之一是可重用性。如果您创建一个ListView类,那么,使用编写良好的代码,您可以让同一个类处理文章下方的用户列表和评论的呈现。因为它们都有相同的表示逻辑。您只需切换模板。
ListView
您可以使用原生 PHP 模板或使用一些第三方模板引擎。也可能有一些第三方库,它们能够完全替换 View 实例。
唯一的主要变化是,旧版本中所谓的 Model ,实际上是一个 Service 。“图书馆类比”的其余部分保持得很好。
我看到的唯一缺陷是这将是一个非常奇怪的图书馆,因为它会从书中返回信息,但不会让你触摸书本身,否则抽象会开始“泄漏”。我可能不得不想一个更贴切的比喻。
MVC结构由两层组成:ui和model。 UI 层 的主要结构是视图和控制器。
当您处理使用 MVC 设计模式的网站时,最好的方法是在视图和控制器之间建立 1:1 的关系。每个视图代表您网站中的整个页面,并且它有一个专用控制器来处理该特定视图的所有传入请求。
例如,要表示一篇打开的文章,您需要\Application\Controller\Document和\Application\View\Document。这将包含 UI 层的所有主要功能,当涉及到处理文章时 _(当然,您可能有一些 与文章没有直接关系的XHR_组件) 。
\Application\Controller\Document
\Application\View\Document